Type Provider for INI file (draft)

namespace Personal.FSharp.TypeProviders

open System
open System.IO
open System.Reflection
open System.Runtime.InteropServices
open System.Text
open Microsoft.FSharp.Core.CompilerServices
open Samples.FSharp.ProvidedTypes

module Win32 =
    [<DllImport("kernel32", CharSet = CharSet.Unicode)>]
    extern int GetPrivateProfileSectionNamesW(
        byte[] lpszReturnBuffer,
        int nSize,
        string lpFileName)

    [<DllImport("kernel32", CharSet = CharSet.Unicode)>]
    extern int GetPrivateProfileSectionW(
        string lpAppName,
        byte[] lpReturnedString,
        int nSize,
        string lpFileName)

    [<DllImport("kernel32", CharSet = CharSet.Unicode)>]
    extern int GetPrivateProfileStringW(
        string lpAppName,
        string lpKeyName,
        string lpDefault,
        StringBuilder lpReturnedString,
        int nSize,
        string lpFileName)

module Util =
    let private SplitNullTerminatedBuffer(buf : byte[], count : int) =
        // wchar用に文字数の倍の数分だけバッファを読む
        let namesBuf = Encoding.Unicode.GetString(buf, 0, count*2)
        namesBuf.Split([| (char)0 |], StringSplitOptions.RemoveEmptyEntries)

    /// <summary>INIファイル内にあるすべてのセクション名を取得します。</summary>
    /// <param name="fileName">INIファイルの名前。</param>
    let GetAllSectionNames(fileName) =
        let buf = Array.zeroCreate 10240
        let numBuffer = Win32.GetPrivateProfileSectionNamesW(buf, buf.Length, fileName)
        SplitNullTerminatedBuffer(buf, numBuffer)

    /// <summary>INIファイル内の特定セクションにあるすべてのキーを取得します。</summary>
    /// <param name="fileName">INIファイルの名前。</param>
    /// <param name="sectionName">INIファイル内にあるセクション名。</param>
    let GetAllKeysInSection(fileName, sectionName) =
        let buf = Array.zeroCreate 10240
        let numBuffer = Win32.GetPrivateProfileSectionW(sectionName, buf, buf.Length, fileName)
        let values = SplitNullTerminatedBuffer(buf, numBuffer)
        seq { for i in 0 .. values.Length - 1 do
                let kvp = values.[i].Split([|'='|], 2)
                yield kvp.[0] }

    /// <summary>INIファイル内の特定セクションにあるキーの値を取得します。</summary>
    /// <param name="fileName">INIファイルの名前。</param>
    /// <param name="sectionName">INIファイル内にあるセクション名。</param>
    /// <param name="keyName">取得したい値を持つキーの名前。</param>
    /// <param name="defaultValue">デフォルト値。</param>
    let GetSectionValue(fileName, sectionName, keyName, defaultValue) =
        let buf = new StringBuilder(10240)
        Win32.GetPrivateProfileStringW(sectionName, keyName, defaultValue, buf, buf.Capacity, fileName) |> ignore
        buf.ToString()

/// <summary>INIファイル内にあるセクションを表します。</summary>
/// <param name="fileName">INIファイルの名前。</param>
/// <param name="sectionName">INIファイル内にあるセクション名。</param>
type IniSection(fileName, sectionName) =
    let keys = seq { for key in Util.GetAllKeysInSection(fileName, sectionName) do yield key }

    /// 現在のセクションの名前。
    member __.Name = sectionName
    /// 現在のセクション内にあるすべてのキー。
    member __.Keys = keys |> Seq.toArray
    /// <summary>
    ///   現在のセクション内にある特定のキーの値を取得します。
    ///   <para>キーが見つからない場合、<paramref name="defaultValue">の値が返されます。</para>
    /// </summary>
    /// <param name="key">取得したい値を持つキーの名前。</param>
    /// <param name="defaultValue">デフォルト値。</param>
    member __.GetValue(key, defaultValue) = Util.GetSectionValue(fileName, sectionName, key, defaultValue)

/// <summary>INIファイルを表します。</summary>
/// <param name="fileName">INIファイルの名前。</param>
type IniFile(fileName) =
    let sections = seq { for name in Util.GetAllSectionNames(fileName) do yield name } |> Seq.cache

    /// INIファイルの名前。
    member __.FileName = fileName
    /// INIファイル内にあるすべてのセクション名。
    member __.Sections = sections |> Seq.toArray
    /// <summary>INIファイル内にある特定のセクションを取得します。</summary>
    /// <param name="name">INIファイル内にあるセクション名。</param>
    member __.GetSection(name) = IniSection(fileName, name)

/// <summary>INIファイル用のType Providerを実装する型。</summary>
/// <param name="config"><see ref="Microsoft.FSharp.Core.CompilerServices.TypeProviderConfig"/>による構成をサポートします。</param>
[<TypeProvider>]
type IniFileTypeProvider(config : TypeProviderConfig) as this =
    inherit TypeProviderForNamespaces()

    let asm = Assembly.GetExecutingAssembly()
    let ns = "Personal.FSharp.TypeProviders"

    // INIファイル用Type Providerの起点となる型の定義。
    // このインスタンスに様々なメンバや型定義を追加していきます。
    let iniTy = ProvidedTypeDefinition(asm, ns, "Ini", Some(typeof<obj>))

    // Type Provider使用時にstatic引数で指定された値。
    // 今回はINIファイルの名前を指定できるようにするので、
    // string型のProvidedStaticParameter1つ用意します。
    let filename = ProvidedStaticParameter("filename", typeof<string>)
    
    // static引数の実体。
    let applyFunc = fun (tyName:string) (parameters:obj[]) ->
        match parameters with
        | [| :? string as filename |] ->
            let ty = ProvidedTypeDefinition(asm, ns, tyName, Some(typeof<IniFile>))

            let resolvedFilename = Path.GetFullPath(Path.Combine(config.ResolutionFolder, filename))
            let iniFile = new IniFile(resolvedFilename)

            // それぞれのセクションと同じ名前のプロパティを追加
            iniFile.Sections
            |> Seq.iter (fun section ->
                let sectionTy = ProvidedTypeDefinition(section, Some(typeof<IniSection>))

                // それぞれのキーと同じ名前のプロパティを追加
                iniFile.GetSection(section).Keys
                |> Seq.iter (fun key ->
                    let keyProp = ProvidedProperty(key, typeof<string>,
                                    GetterCode = fun args -> <@@ (%%args.[0] : IniSection).GetValue(key, null) @@>)
                    sectionTy.AddMember keyProp)

                let prop = ProvidedProperty(section, sectionTy,
                            GetterCode = fun args -> <@@ (%%args.[0] : IniFile).GetSection(section) @@>)
                ty.AddMember prop

                ty.AddMember sectionTy)

            let ctor0 = ProvidedConstructor([],
                            InvokeCode = fun [] -> <@@ IniFile(resolvedFilename) @@>)
            ty.AddMember ctor0

            ty
        | _ -> failwith "Invalid parameter"

    do iniTy.DefineStaticParameters([filename], applyFunc)

    do this.AddNamespace(ns, [ iniTy ])

[<assembly:TypeProviderAssembly>]
do ()