// Copyright (c) Microsoft Open Technologies, Inc.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information.

namespace Microsoft.FSharp.Data.TypeProviders.DesignTime

open System
open System.IO
open System.Reflection
open System.Collections.Generic
open System.Linq
open System.Xml.Linq
open System.Net
open System.Configuration
open Microsoft.FSharp.Data.TypeProviders.Utility

[<AutoOpen>]
module internal Utilities = 

    let validateSchemaFileExtension localSchemaFile ext = 
        // treat extensions that differ only in case as equal
        if not (Path.GetExtension(localSchemaFile).Equals("."+ext, StringComparison.OrdinalIgnoreCase)) then 
            failwith (FSData.SR.errorInvalidExtensionSchema(Path.GetFileName(localSchemaFile),ext))

    /// Use a local schema file if the 'localSchemaFile' parameter is present (IsNullOrWhiteSpace). Otherwise,
    /// use a temporary file to hold the metadata description of the service. Always force the update
    /// of the file if 'forceUpdate' is true and the downloaded contents have changed. 
    let tryUseLocalSchemaFile (localSchemaFile, forceUpdate, ext) loader =
        let useTempFile = String.IsNullOrWhiteSpace localSchemaFile
        let schemaFile = if useTempFile then Util.TemporaryFile ext else Util.ExistingFile localSchemaFile

        // Check if the extension of LocalSchema matches the given "ext"
        if not useTempFile then validateSchemaFileExtension localSchemaFile ext

        if useTempFile || forceUpdate || not (File.Exists schemaFile.Path) then 
            let metadataText = Util.TemporaryFile ext |> loader

            // Only write if it's actually changed
            if useTempFile || (try File.ReadAllText schemaFile.Path with _ -> null) <> metadataText then 
                try 
                   File.WriteAllText(schemaFile.Path, metadataText)
                with :? System.IO.IOException as e -> failwith (FSData.SR.errorWritingLocalSchemaFile(e.Message))

        schemaFile

    // tricky moment that error message can spread through multiple lines
    // so we:
    // - walk through lines until we met first line with error code, save this line        
    // - traverse lines till the next error code (or EOF). append every line to the buffer
    // - flush buffer on every line with error code (or at the end)
    let private selectLines hasErrorCode (arr : string[]) = 

        // sqlmetal\edmgen pads inner exceptions with tabs so if line starts with tab - this is not beginning of the message
        // svcutil pads inner exceptions with '    ' so this is not message start as well
        let (|MessageStart|_|) line = 
            if not (System.String.IsNullOrEmpty line) && line.[0] <> '\t' && not (line.StartsWith "    ") && hasErrorCode line then Some () else None
        
        let (|EmptyLine|_|) line = 
            if String.IsNullOrEmpty line then Some () else None
        
        [|
            let buf = System.Text.StringBuilder()
            for line in arr do
                match line with
                | MessageStart -> 
                    // return accumulated result with the prevous message
                    if buf.Length <> 0 then
                        yield buf.ToString()
                        buf.Length <- 0
                    // start new message
                    buf.Append(line.Trim()) |> ignore
                | EmptyLine ->
                    // // we assume that messages cannot contain embedded empty lines so empty line terminates current message
                    if buf.Length <> 0 then
                        yield buf.ToString()
                        buf.Length <- 0
                | l ->
                    // if we already started writing the message - then interleave lines in buffer with spaces
                    // if buffer.Length = 0 - this means that we previous error has already ended but MessageStart for the new one wasn't yet met.
                    // in this case we discard this line - it is probably header or footer of output
                    if buf.Length <> 0 then
                        buf
                            .Append(" ")
                            .Append(l.Trim()) |> ignore

            // flush remaining content
            if buf.Length <> 0 then
                yield buf.ToString()                       
        |]

    // Defines the main pattern for matching messages.
    // taken from vsproject\xmake\shared\canonicalerror.cs
    let private canonicalMessageRegex = 
        new System.Text.RegularExpressions.Regex            
                  (
                     // Beginning of line and any amount of whitespace.
                     @"^\s*" +
                     // Match a [optional project number prefix 'ddd>'], single letter + colon + remaining filename, or
                     // string with no colon followed by a colon.
                     @"(((?<ORIGIN>(((\d+>)?[a-zA-Z]?:[^:]*)|([^:]*))):)" +
                     // Origin may also be empty. In this case there's no trailing colon.
                     "|())" +
                     // Match the empty string or a string without a colon that ends with a space
                     "(?<SUBCATEGORY>(()|([^:]*? )))" +
                     // Match 'error' or 'warning'.
                     @"(?<CATEGORY>(error|warning))" +
                     // Match anything starting with a space that's not a colon/space, followed by a colon. 
                     // Error code is optional in which case "error"/"warning" can be followed immediately by a colon.
                     @"( \s*(?<CODE>[^: ]*))?\s*:" +
                     // Whatever's left on this line, including colons.
                     "(?<TEXT>.*)$",
                     System.Text.RegularExpressions.RegexOptions.IgnoreCase ||| System.Text.RegularExpressions.RegexOptions.Compiled
                  )

    // Defines the additional pattern for matching messages which may contain localized strings.
    // Note that the string "error|warning" from canonicalMessageRegex is simply replaced by "\w+"
    // to match both any localized strings and PLOC'd strings.
    // It has some group misalignment for PLOC'd strings, but as a whole, it works as expected.
    let private structuralMessageRegex = 
        new System.Text.RegularExpressions.Regex            
                  (
                     // Beginning of line and any amount of whitespace.
                     @"^\s*" +
                     // Match a [optional project number prefix 'ddd>'], single letter + colon + remaining filename, or
                     // string with no colon followed by a colon.
                     @"(((?<ORIGIN>(((\d+>)?[a-zA-Z]?:[^:]*)|([^:]*))):)" +
                     // Origin may also be empty. In this case there's no trailing colon.
                     "|())" +
                     // Match the empty string or a string without a colon that ends with a space
                     "(?<SUBCATEGORY>(()|([^:]*? )))" +
                     // Match localized 'error' or 'warning'.
                     @"(?<CATEGORY>(\w+))" +
                     // Match anything starting with a space that's not a colon/space, followed by a colon. 
                     // Error code is optional in which case "error"/"warning" can be followed immediately by a colon.
                     // constraints for CODE here is more restrictive than in cannonical case - here we match only alphanumeric chars
                     @"( \s*(?<CODE>\w*))?\s*:" +
                     // Whatever's left on this line, including colons.
                     "(?<TEXT>.*)$",
                     System.Text.RegularExpressions.RegexOptions.IgnoreCase ||| System.Text.RegularExpressions.RegexOptions.Compiled
                  )

    let formatErr (stdout: string[]) (stderr: string[])  : unit  =
        let getErrors s = 
            Array.ofSeq s
            |> selectLines (fun s -> 
                if canonicalMessageRegex.IsMatch s then true
                else 
                    // structural regex can produce false positives when line contain ':' chars
                    // to reduce amount of false positives we'll mangle the most frequent scenario when ':' can be met - urls.
                    // In addition, "!" which wraps PLOC'd strings will be also replaced to "_" to make the matching easier. 
                    let escapedLine = s.Replace("://", "_").Replace("!","_")
                    structuralMessageRegex.IsMatch escapedLine
                )
            |> Array.map Failure
        let exns = 
            Array.concat 
                [
                    getErrors stdout
                    getErrors stderr
                ] 
        assert (exns.Length <> 0)
        raise (AggregateException exns)
            
    /// Run SQLMetal and strip out a decent error message
    let sqlmetal args = Util.shell (Path.GetTempPath(), Util.sdkUtil "SqlMetal.exe", args, Some formatErr)

    let inline handleReadingSchemaError (e : exn) = 
        match e with
        | :? AggregateException as ae -> 
            let inners = 
                ae.InnerExceptions
                |> Seq.map (fun e -> Failure(FSData.SR.errorReadingSchema(e.Message)))
                |> Seq.toArray

            raise(AggregateException(inners))
        | e -> failwith (FSData.SR.errorReadingSchema(e.Message))

    // will exists forever - but I guess we can live with it as the leak is minor
    // CSharpCodeProvider is IDisposable - 
    // Dispose is inherited from Component: removes component from the ISite and unsubscribes from events - we don't use any of this features    
    let private csCodeProvider = new Microsoft.CSharp.CSharpCodeProvider()
    
    let validateDataContextClassName name = 
        if not(String.IsNullOrWhiteSpace name) && not (csCodeProvider.IsValidIdentifier name) then
            failwith (FSData.SR.invalidDataContextClassName(name))

    type UC = System.Globalization.UnicodeCategory

    let sanitizeDataContextFileName (name : string) = 
        if String.IsNullOrEmpty name then failwith (FSData.SR.invalidDataContextClassName(name))
        
        /// tests if specified character is valid start character for C# identifier
        let isValidStartChar c = Char.IsLetter c || c = '_'

        /// tests if specified character is valid non-start character for C# identifier
        let isValidNonStartChar c = 
            if Char.IsLetterOrDigit c || c = '_' then true
            else
            match Char.GetUnicodeCategory(c) with
            | UC.NonSpacingMark | UC.ConnectorPunctuation | UC.SpacingCombiningMark -> true
            | _ -> false

        let buf = System.Text.StringBuilder()
    
        let name = 
            // convert given identifier to the uniform representation
            // 1. make it all lower-cased
            // 2. convert all word starting letters to upper-case + remove whitespaces
            let name = name.ToLower()
            let buf = System.Text.StringBuilder()        
            let rec capitalizeFirstLetters i convertNextLetterToUpper = 
                if i >= name.Length then buf.ToString()
                else
                let c = name.[i]
                if Char.IsWhiteSpace c then 
                    // whitespace - set convertNextLetterToUpper to true and proceed to the next character (thus removing whitespace from the result buffer)
                    capitalizeFirstLetters (i + 1) true
                else
                    // check if we see whitespace before.
                    // if yes and current character is letter - it should be converted it to upper case before appending character to the result buffer
                    let c = if Char.IsLetter c && convertNextLetterToUpper then Char.ToUpper c else c
                    buf.Append(c) |> ignore
                    capitalizeFirstLetters (i + 1) false

            capitalizeFirstLetters 0 true

        let startIndex = 
            let first = name.[0]
            if isValidStartChar first then 
                // if first character is valid start character for C# identifier => append it to the result buffer
                buf.Append(first) |> ignore
                1
            elif isValidNonStartChar first then
                // first character in name is not valid start character but valid as non-start. => it should be prepended with '_'
                buf.Append('_').Append(first) |> ignore
                1
            else 
                // first character is invalid for usage in C# identifier => let loop below do all job (this char will be replaced with '_')
                0
        
        // check char by char if they can be used in C# identifier. invalid characters are replaced with '_'
        for i=startIndex to (name.Length - 1) do
            let c = name.[i]
            let c = if isValidNonStartChar c then c else '_'
            buf.Append(c) |> ignore

        // pad result with underscores if it occasionaly matches some C# keyword or reserved word
        csCodeProvider.CreateValidIdentifier(buf.ToString())   

module internal ConfigFiles =
    
    type ConfigFileSearchResult = 
        | StandardFound of string
        | CustomFound of string
        | StandardNotFound
        | CustomNotFound of string

    /// searches for the configuration file that will be used by TP
    /// designTimeDirectory- root folder
    /// configFileName - full path to the custom configuration file
    let findConfigFile (designTimeDirectory, configFileName) = 
        if String.IsNullOrWhiteSpace configFileName then 
            let appConfig = System.IO.Path.Combine(designTimeDirectory,"app.config") 
            let webConfig = System.IO.Path.Combine(designTimeDirectory,"web.config") 
            if System.IO.File.Exists appConfig then StandardFound appConfig
            elif System.IO.File.Exists webConfig then StandardFound webConfig
            else StandardNotFound
        else 
            if System.IO.File.Exists configFileName then 
                CustomFound configFileName
            else 
                CustomNotFound configFileName

    [<RequireQualifiedAccess>]
    type ConnectionStringReadResult = 
        | Ok of ConnectionStringSettings
        | NotFound
        | Error of exn

    /// reads ConnectionStringSettings with name <connectionStringName> from the specified file
    /// returns either ConnectionStringReadResult.Ok (cs : ConnectionStringSettings) (cs is not null)
    /// or ConnectionStringReadResult.NotFound if configuration file doesn't contain ConnectionStringSettings with specified name
    /// or ConnectionStringReadResult.Error if reading of the file ends with error
    let tryReadConnectionString (configFileName : string, connectionStringName : string) = 
        try 
            // copy original file to avoid its locking
            let text = System.IO.File.ReadAllText configFileName
            use tmpConfigFile = Util.TemporaryFile  "config"
            do System.IO.File.WriteAllText(tmpConfigFile.Path,text)
            let map = ExeConfigurationFileMap(ExeConfigFilename = tmpConfigFile.Path)
            let config = ConfigurationManager.OpenMappedExeConfiguration(map, ConfigurationUserLevel.None);

            let connString = config.ConnectionStrings.ConnectionStrings.[connectionStringName]
            if connString <> null then ConnectionStringReadResult.Ok connString
            else ConnectionStringReadResult.NotFound
        with exn -> 
            ConnectionStringReadResult.Error exn


    let computeStaticConnectionString (name,checkForSqlClientProvider, connectionString, connectionStringName, configFileName, absoluteDataDirectoryParam, designTimeDirectory) = 
        let connectionString, usingConfigFileInfo = 
            if String.IsNullOrWhiteSpace connectionString then 

                let configFileName, configFileBaseNameOpt = 
                    match findConfigFile (designTimeDirectory, configFileName) with
                    | StandardFound path ->
                        let name = Path.GetFileName path
                        path, Some name
                    | CustomFound path -> path, None
                    | StandardNotFound -> failwith (FSData.SR.noConfigFileFound1())
                    | CustomNotFound name -> failwith (FSData.SR.noConfigFileFound2(name))

                if String.IsNullOrWhiteSpace connectionStringName then 
                    failwith (FSData.SR.noConnectionStringOrConnectionStringName(name))
                
                let mkReadConnectionStringError msg = 
                    FSData.SR.errorWhileReadingConnectionStringInConfigFile (connectionStringName, configFileName, msg)
                    |> Choice2Of2

                let result = 
                    match tryReadConnectionString(configFileName, connectionStringName) with
                    | ConnectionStringReadResult.Ok connString ->                        
                        if checkForSqlClientProvider && connString.ProviderName <> "System.Data.SqlClient" then 
                            Choice2Of2 (FSData.SR.invalidProviderInConfigFile (connString.ProviderName, connectionStringName, configFileName))

                        else
                            let result = connString.ConnectionString
                            if String.IsNullOrWhiteSpace result then 
                                Choice2Of2 (FSData.SR.invalidConnectionStringInConfigFile (result, connectionStringName, configFileName))
                            else
                                Choice1Of2  result
                    | ConnectionStringReadResult.NotFound -> 
                        FSData.SR.connectionStringNotFound(connectionStringName)
                        |> mkReadConnectionStringError

                    | ConnectionStringReadResult.Error exn -> 
                        mkReadConnectionStringError exn.Message

                match result with 
                | Choice1Of2 r -> r, Some (connectionStringName, configFileBaseNameOpt)
                | Choice2Of2 msg -> failwith msg
            else
                if not (String.IsNullOrWhiteSpace connectionStringName) then 
                    failwith (FSData.SR.notBothConnectionStringOrConnectionStringName())
                connectionString, None

        // Always do |DataDirectory| substitution
        let connectionString = 
            let absoluteDataDirectory = 
                if String.IsNullOrWhiteSpace absoluteDataDirectoryParam then 
                    designTimeDirectory 
                else 
                    if not (System.IO.Directory.Exists absoluteDataDirectoryParam) then 
                        failwith (FSData.SR.dataDirectoryNotFound(absoluteDataDirectoryParam))
                    absoluteDataDirectoryParam            
            connectionString.Replace("|DataDirectory|", absoluteDataDirectory)
        
        connectionString, usingConfigFileInfo

module internal Dbml =

    let buildType (dbmlFileContent: string, contextTypeName:string, serializable:bool)  =
        
        validateDataContextClassName contextTypeName

        // Remove the Database/Connection element if exist, which can be created by the O/R Designer
        // Its attributes, "SettingsObjectName" and "SettingsPropertyName" can cause sqlmetal to generate a problematic csharp file 
        let dbmlFileContent =
            if dbmlFileContent.Contains("Connection") then
                let xdoc = XDocument.Parse dbmlFileContent
                xdoc.Elements()
                |> Seq.filter (fun xnode -> xnode.Name.LocalName = "Database")
                |> Seq.choose (fun xnode -> 
                    match xnode.Elements(xnode.Name.Namespace + "Connection") with 
                    | null -> None 
                    | s -> Some (s))
                |> Seq.iter (fun xchild -> xchild.Remove())
                xdoc.ToString()
            else dbmlFileContent
                    
        use csFile = Util.TemporaryFile "cs"
        use dbmlFile = Util.TemporaryFile "dbml"
        File.WriteAllText (dbmlFile.Path, dbmlFileContent)

        let sqlMetalArgs = 
            let command = sprintf "/code:\"%s\" /language:C#  \"%s\"" csFile.Path dbmlFile.Path
            let command = if not (String.IsNullOrWhiteSpace contextTypeName) then sprintf "%s /context:\"%s\" " command contextTypeName else command
            let command = if serializable then sprintf "%s /serialization:Unidirectional" command else command
            command

        sqlmetal sqlMetalArgs

        let assemblyFile = Util.TemporaryFile "dll" // TODO: load in-memory and clean up this file

        let cscCommand = sprintf "/nologo /target:library /out:\"%s\" \"%s\"" assemblyFile.Path csFile.Path
        Util.shell(Path.GetTempPath(), Util.cscExe(), cscCommand, None) 
            
        assemblyFile

module internal SqlConnection =

    let getDbml (dbmlFile: Util.FileResource, connString, flagPluralize, flagViews, flagFunctions, flagStoredProcs, flagTimeout, contextTypeName) = 
        
        validateDataContextClassName contextTypeName
        
        let sqlMetalArgs = 
            let command = sprintf "/conn:\"%s\" /dbml:\"%s\"" connString dbmlFile.Path
            let command = if flagPluralize   then sprintf "%s /pluralize" command else command
            let command = if flagViews       then sprintf "%s /views"     command else command
            let command = if flagFunctions   then sprintf "%s /functions" command else command
            let command = if flagStoredProcs then sprintf "%s /sprocs"    command else command
            let command = if not (String.IsNullOrWhiteSpace contextTypeName)    then sprintf "%s /context:\"%s\" " command contextTypeName else command
            let command = sprintf "%s /timeout:%d" command flagTimeout 
            command
        sqlmetal sqlMetalArgs
        File.ReadAllText dbmlFile.Path


    let buildType (connectionString, connectionStringName:string, configFileName:string, absoluteDataDirectory:string, localSchemaFile:string, forceUpdate:bool, flagPluralize, flagViews, flagFunctions, flagStoredProcs, flagTimeout, contextTypeName, serializable, designTimeDirectory)  =
        let connectionString, usingConfigFileInfo = ConfigFiles.computeStaticConnectionString ("SqlDataConnection", true, connectionString, connectionStringName, configFileName, absoluteDataDirectory, designTimeDirectory)
        
        // parses connection string on components, validates only syntax
        let connStringBuilder = System.Data.Common.DbConnectionStringBuilder()
        connStringBuilder.ConnectionString <- connectionString
        let contextTypeName = 
            // if context is not explicitly set - try to infer context name from database filename
            if String.IsNullOrWhiteSpace(contextTypeName) then
                let dbFileNameOpt = 
                    // SqlCE allows usage of both 'Data Source' and 'DataSource' names and treats them as equivalent
                    // however SqlMetal doesn't recognize 'DataSource' and always looks for 'Data Source'
                    // so to figure out if given connection string is SqlCE - we'll get value of 'Data Source' and check if it ends with .sdf
                    // NOTE: we cannot just use SqlConnectionStringBuilder because it rejects some SqlCE specific parameter names
                    match connStringBuilder.TryGetValue ("Data Source") with
                    | true, (:? string as ds) when ds.EndsWith(".sdf", System.StringComparison.OrdinalIgnoreCase) -> Some ds // SqlCE
                    | _ -> 
                        // SqlServer
                        let sqlConnString = System.Data.SqlClient.SqlConnectionStringBuilder(connectionString)
                        if not (String.IsNullOrEmpty sqlConnString.AttachDBFilename) then
                            Some (sqlConnString.AttachDBFilename)
                        else 
                            None

                match dbFileNameOpt with
                | Some f -> Path.GetFileNameWithoutExtension f |> sanitizeDataContextFileName
                | None -> contextTypeName
            else
                contextTypeName

        use dbmlFile = 
            tryUseLocalSchemaFile 
                (localSchemaFile, forceUpdate, "dbml") 
                (fun dbmlFile -> 
                  try 
                    getDbml (dbmlFile,connectionString, flagPluralize, flagViews, flagFunctions, flagStoredProcs, flagTimeout, contextTypeName)
                  with 
                    e -> handleReadingSchemaError e)

        use csFile = Util.TemporaryFile "cs"
        let sqlMetalArgs = 
            let command = sprintf "/code:\"%s\" /language:C#  \"%s\"" csFile.Path dbmlFile.Path 
            let command = if not (String.IsNullOrWhiteSpace contextTypeName)    then sprintf "%s /context:\"%s\" "    command contextTypeName else command
            let command = if serializable then sprintf "%s /serialization:Unidirectional" command else command
            command
        sqlmetal sqlMetalArgs
        
        let assemblyFile = Util.TemporaryFile "dll" // TODO: load in-memory and clean up this file

        let cscCommand = sprintf "/nologo /target:library /out:\"%s\" \"%s\"" assemblyFile.Path csFile.Path
        Util.shell(Path.GetTempPath(), Util.cscExe(), cscCommand, None) 
            
        assemblyFile, usingConfigFileInfo




module internal DataSvcUtil =

    type MetadataError = FixedQueryNotSupported | DataQualityServiceNotSupported
    module Metadata = 
    
        let private edmx = XNamespace.Get "http://schemas.microsoft.com/ado/2007/06/edmx"
        let private dr = XNamespace.Get "http://schemas.microsoft.com/dallas/2010/04"
        let private edm08 = XNamespace.Get "http://schemas.microsoft.com/ado/2009/08/edm"
        let private edm05 = XNamespace.Get "http://schemas.microsoft.com/ado/2007/05/edm"

        let private get (name : XName) (c : XContainer) = 
            match c.Element name with
            | null -> None
            | el -> Some (el :> XContainer)
        
        /// Analyzes given metadata file and tries to find evidence of specific cases that may cause datasvcutil to fail
        let TryPredictError (path : string) = 
            try
                let xdoc = XDocument.Load(path)
                let error =                     
                    defaultArg (get (edmx + "Edmx") xdoc) (xdoc :> XContainer)
                    |> get (edmx + "DataServices")
                    |> Option.bind (fun ds -> 
                        match get (edm08 + "Schema") ds with
                        | Some el -> Some (el, edm08)
                        | None -> 

                        match get (edm05 + "Schema") ds with
                        | Some el -> Some (el, edm05)
                        | None -> None
                    )
                    |> Option.bind (fun (schema, ns) ->
                        // check if metadata contains node
                        // <Using Namespace="Microsoft.DQS">
                        // this indicates that target service implements DataQuality API - datasvcutil cannot consume it correctly
                        let isDQS = 
                            match get (ns + "Using") schema with
                            | Some (:? XElement as e) -> 
                                match e.Attribute(XName.Get "Namespace") with
                                | null -> false
                                | attr -> attr.Value = "Microsoft.DQS"
                            | _ -> false
                        
                        if isDQS then Some DataQualityServiceNotSupported
                        else

                        // check all EntityType nodes
                        // if all of them has <Property Queryable='false'> - then it may indicate that service support fixed query
                        let entityTypes = schema.Elements(ns + "EntityType")
                        let isFixed = 
                            entityTypes.Elements (ns + "Property")
                            |> Seq.forall (fun el ->
                                let attr = el.Attribute(dr + "Queryable")
                                attr <> null && attr.Value = "false"
                                )

                        if isFixed then Some FixedQueryNotSupported 
                        else None
                    )
                error
            with _ -> None // swallow this error (if datasvcutil will fail - we'll report errors from the output) 

    /// Add the default service URI to the generated CS file by inserting it as the argument
    /// to the default data context constructor, also drop the namespace declaration.
    let addDefaultUriToDataSvcUtilCSharpFile uri csFile = 
        let csText = File.ReadAllLines csFile

        let (|DataContextClass|_|) (line:string) =
            let trimmed = line.Trim('{',' ','\t') 
            let starting = "public partial class "
            let ending = "System.Data.Services.Client.DataServiceContext"
            if trimmed.StartsWith starting && trimmed.Contains ":" && trimmed.EndsWith ending then
                let s = trimmed.Substring starting.Length
                Some (s.Substring(0, s.IndexOf ' '), line.Contains "{")
            else None

        let doctoredText =
            [|  // Was the last line a namespace?
                let prevNS = ref false
                let foundDataContextClass = ref None
                for line in csText do
                    match line with
                    |   _ when prevNS.Value -> prevNS := false // note, the line after the namespace is dropped
                    |   _ when line.StartsWith "namespace" -> prevNS := true // note, the namespace declaration is dropped

                    |   DataContextClass (className, endsWithBrace) ->
                            foundDataContextClass := Some (className, endsWithBrace)
                            yield line
                    |   _ -> 
                         match line.Trim(), foundDataContextClass.Value with 
                         | "{", Some(className, false) 
                         | "", Some(className, true) ->
                            yield line
                            yield sprintf "public %s() : this(new global::System.Uri(\"%s\")) {}" className uri
                            foundDataContextClass := None

                         |   _ when line <> "}" -> yield line
                         |   _ -> ()
            |] 
        File.WriteAllLines(csFile, doctoredText)

    let buildTypeFromMetadataUri (uri : string, localSchemaFile:string, forceUpdate:bool, dataServiceCollection) =

        use csdlFile = 
         tryUseLocalSchemaFile (localSchemaFile, forceUpdate, "csdl") (fun _ -> 
            try 
                let metadataUri =
                    if uri.EndsWith "/" then uri + "$metadata"
                    else uri + "/$metadata"
                let metadataText = 
                    use c = new WebClient(UseDefaultCredentials=true) // TODO: consider adding schema credentials here 
                    c.DownloadString metadataUri
                metadataText
            with e -> handleReadingSchemaError e)
        
        let expectedError = Metadata.TryPredictError csdlFile.Path

        use csFile = Util.TemporaryFile "cs"
        let dataSvcUtilArgs = 
            let args = sprintf "/nologo /in:\"%s\" /out:\"%s\" /language:CSharp /version:2.0" csdlFile.Path csFile.Path
            let args = if dataServiceCollection then sprintf "%s /DataServiceCollection" args else args
            args
        try
            Util.shell(Path.GetTempPath(), Util.dataSvcUtilExe(), dataSvcUtilArgs, Some formatErr)
        with
            _ ->
                // check if we expect errors in this file
                match expectedError with
                | Some FixedQueryNotSupported -> failwith (FSData.SR.fixedQueriesNotSupported())
                | Some DataQualityServiceNotSupported -> failwith (FSData.SR.dqsServicesNotSupported())
                | None -> reraise()

        addDefaultUriToDataSvcUtilCSharpFile uri csFile.Path
        
        let assemblyFile = Util.TemporaryFile "dll" // TODO: load in-memory and clean up this file
        
        let cscCommand = sprintf "/nologo /target:library /r:System.Data.Services.Client.dll /out:\"%s\" \"%s\"" assemblyFile.Path csFile.Path
        Util.shell(Path.GetTempPath(), Util.cscExe(), cscCommand, None)             
        assemblyFile


module internal SvcUtil =
    
    let serviceMetadataFilesXName = XName.Get "ServiceMetadataFiles"
    let serviceMetadataFileXName = XName.Get "ServiceMetadataFile"
    let fileNameXName = XName.Get "name"

    /// assumes that destFolder is already created
    let private unpackFolder (sourceFile : string) (destFolder : string) =
        let xml = XDocument.Load sourceFile
        for fileNode in xml.Root.Elements(serviceMetadataFileXName) do
            let name = fileNode.Attribute(fileNameXName)
            let resultPath = Path.Combine(destFolder, name.Value)
            let root = 
                fileNode.Elements() 
                |> Seq.tryFind(fun _ -> true) // pick first element
            match root with
            | Some root -> root.Save(resultPath)
            | None -> failwith (FSData.SR.serviceMetadataFileElementIsEmpty())

    let private packFolder sourceFolder (destFile : string) patterns =
        let root =XElement(serviceMetadataFilesXName)
        let doc = XDocument(root)

        let append pat = 
            for f in Directory.GetFiles(sourceFolder, pat) do
                let xml = XDocument.Load(f)
                let node = XElement(serviceMetadataFileXName)
                let fileName = Path.GetFileName f
                // add attribute with the name of source file
                node.Add(XAttribute(fileNameXName, fileName))
                // append file content
                node.Add(xml.Root)

                root.Add(node)

        for pat in patterns do
            append pat

        doc.Save(destFile)

    let validateWsdlUri uri = 
        if String.IsNullOrWhiteSpace uri then
            failwith (FSData.SR.invalidWsdlUri())

    let buildTypeFromWsdlUri (namespaceName, wsdlUri : string, localSchemaFile:string, forceUpdate:bool, messageContract, enableDataBinding, serializable, async, collectionType) =
        
        validateWsdlUri wsdlUri

        use metadataDir = 
            let tempFolder = Util.TemporaryDirectory()
            
            let schemaFileSet = not (String.IsNullOrWhiteSpace localSchemaFile)
            let schemaFileExists = schemaFileSet && ( try File.Exists(localSchemaFile) with _ -> false )

            if schemaFileSet then validateSchemaFileExtension localSchemaFile "wsdlschema"

            // downloads metadata to the specified temp folder
            // if localSchemaFile is set - pack downloaded content as localSchemaFile
            let downloadMetadata () = 
                if (try System.IO.Directory.Exists tempFolder.Path with _ -> false) then 
                    (try System.IO.Directory.Delete (tempFolder.Path, true) with _ -> ())
                if not (System.IO.Directory.Exists tempFolder.Path) then 
                    System.IO.Directory.CreateDirectory tempFolder.Path |> ignore
                let svcUtilArgs = 
                    let args = sprintf "/nologo /t:metadata \"%s\"" wsdlUri 
                    args
        
                Util.shell(tempFolder.Path, Util.svcUtilExe(), svcUtilArgs, Some formatErr)

                if schemaFileSet then
                    packFolder tempFolder.Path localSchemaFile ["*.wsdl"; "*.xsd"]
            
            if not (schemaFileExists) || forceUpdate then
                downloadMetadata()
            else
                // reraise exceptions if local schema file is corrupted
                try
                    unpackFolder localSchemaFile tempFolder.Path 
                with
                    e -> failwith (FSData.SR.errorReadingSchema(e.Message))


            tempFolder

        use configFile = Util.TemporaryFile "config" //Util.ExistingFile (System.IO.Path.Combine(metadataDir.Path, configFileName))
        use csFile = Util.TemporaryFile "cs"

        let svcUtilArgs = 
            let args = sprintf "/nologo  /out:\"%s\" /config:\"%s\" /language:CSharp /serializer:Auto /fault" csFile.Path  configFile.Path
            let args = if System.IO.Directory.EnumerateFiles(metadataDir.Path, "*.wsdl") |> Seq.isEmpty then args else sprintf "%s \"%s\"" args (Path.Combine(metadataDir.Path,"*.wsdl"))
            let args = if System.IO.Directory.EnumerateFiles(metadataDir.Path, "*.xsd") |> Seq.isEmpty then args else sprintf "%s \"%s\"" args (Path.Combine(metadataDir.Path,"*.xsd"))
            let args = if messageContract                           then sprintf "%s /messageContract"   args else args
            let args = if enableDataBinding                         then sprintf "%s /enableDataBinding" args else args
            let args = if serializable                              then sprintf "%s /serializable"      args else args
            let args = if async                                     then sprintf "%s /async"             args else args
            let args = if not (String.IsNullOrWhiteSpace collectionType) then sprintf "%s /collectionType:%s" args collectionType else args
            args
        
        Util.shell(Path.GetTempPath(), Util.svcUtilExe(), svcUtilArgs, Some formatErr)
        
        let assemblyFile = Util.TemporaryFile "dll" // TODO: load in-memory and clean up this file
        
        let cscCommand = sprintf "/nologo /target:library /resource:\"%s\",%s.config,public /out:\"%s\" \"%s\"" configFile.Path namespaceName assemblyFile.Path csFile.Path
        Util.shell(Path.GetTempPath(), Util.cscExe(), cscCommand, None)             

        let endPointNames = 
            try 
                let config = ConfigurationManager.OpenMappedExeConfiguration(ExeConfigurationFileMap(ExeConfigFilename = configFile.Path), ConfigurationUserLevel.None);
                [ for ep in ((config.GetSectionGroup "system.serviceModel").Sections.["client"] :?> System.ServiceModel.Configuration.ClientSection).Endpoints do 
                    yield ep.Name ]
            with _ -> 
                []

        assemblyFile, endPointNames

module internal Edmx =
    type private EdmxNamespaceGroup = { Edmx : XNamespace; Edm : XNamespace; Ssdl :XNamespace; Msl : XNamespace; RequireDotNet45 : bool }
    let private makeGroup (edmx : string, edm : string, ssdl : string, msl : string, requireDotNet45)= 
        { Edmx = XNamespace.Get edmx
          Edm = XNamespace.Get edm
          Ssdl = XNamespace.Get ssdl
          Msl = XNamespace.Get msl 
          RequireDotNet45 = requireDotNet45 }

    let private ns45 = 
        makeGroup 
            (
                "http://schemas.microsoft.com/ado/2009/11/edmx",
                "http://schemas.microsoft.com/ado/2009/11/edm",
                "http://schemas.microsoft.com/ado/2009/11/edm/ssdl",
                "http://schemas.microsoft.com/ado/2009/11/mapping/cs",
                true
             )

    let private ns40 =
        makeGroup 
            (
                "http://schemas.microsoft.com/ado/2008/10/edmx",
                "http://schemas.microsoft.com/ado/2008/09/edm",
                "http://schemas.microsoft.com/ado/2009/02/edm/ssdl",
                "http://schemas.microsoft.com/ado/2008/09/mapping/cs",
                false
             )

    let private ns20 =
        makeGroup 
            (
                "http://schemas.microsoft.com/ado/2007/06/edmx",
                "http://schemas.microsoft.com/ado/2006/04/edm",
                "http://schemas.microsoft.com/ado/2006/04/edm/ssdl",
                "urn:schemas-microsoft-com:windows:storage:mapping:CS",
                false
             )

    let loadEdmxFile path = 
        let getElement (c : XContainer) (name : string) (ns : XNamespace) = 
            match c.Element (ns + name) with
            | null -> failwith (FSData.SR.fileDoesNotContainXMLElement(path, name))
            | el -> el

        let xdoc = 
            match XDocument.Load path with
            | null -> failwith (FSData.SR.failedToLoadFileAsXML(path))
            | xdoc -> xdoc
        let extractEdmxComponents (nsGroup : EdmxNamespaceGroup) = 
            let edmxNode = getElement xdoc "Edmx" nsGroup.Edmx
            let runtimeNode = getElement edmxNode "Runtime" nsGroup.Edmx
            let csdlNode = 
                let conceptualModelNode = getElement runtimeNode "ConceptualModels" nsGroup.Edmx
                getElement conceptualModelNode "Schema" nsGroup.Edm
            let ssdlNode = 
                let storageModelNode = getElement runtimeNode "StorageModels" nsGroup.Edmx
                getElement storageModelNode "Schema" nsGroup.Ssdl
            let mslNode = 
                let mappingsNode = getElement runtimeNode "Mappings" nsGroup.Edmx
                getElement mappingsNode "Mapping" nsGroup.Msl
            (csdlNode.ToString()), (ssdlNode.ToString()), (mslNode.ToString()), nsGroup.RequireDotNet45

        // tries to process given edmx using supplied set of namespace groups
        let rec load = function
            | [] -> assert false; failwith "impossible"
            | x::xs -> 
                try extractEdmxComponents x 
                with e ->
                    match xs with
                    | [] -> reraise() // we were not able to load file - raise exception
                    | _ -> load xs
        load [ns45; ns40; ns20]

    let buildTypeFromEdmxFile (edmxFile, isTargetingDotNet45) =

        use csDir = Util.TemporaryDirectory()
        let baseContainerName = Path.GetFileNameWithoutExtension edmxFile
        let csFile = Path.Combine(csDir.Path, baseContainerName + ".cs")

        let csdlFileName = baseContainerName + ".csdl"
        let mslFileName = baseContainerName + ".msl"
        let ssdlFileName = baseContainerName + ".ssdl"

        let csdlFile = Path.Combine(csDir.Path, csdlFileName)
        let mslFile = Path.Combine(csDir.Path, mslFileName)
        let ssdlFile = Path.Combine(csDir.Path, ssdlFileName)
        let csdlContent, ssdlContent, mslContent, requireDotNet45 = loadEdmxFile edmxFile

        if requireDotNet45 && not isTargetingDotNet45 then
            failwith (FSData.SR.edmxFileRequiresDotNet45(edmxFile))

        File.WriteAllText(csdlFile, csdlContent)
        File.WriteAllText(mslFile, mslContent)
        File.WriteAllText(ssdlFile, ssdlContent)

        let edmGenArgs = 
            let args = sprintf "/mode:EntityClassGeneration /incsdl:\"%s\" /outobjectlayer:\"%s\" /nologo" csdlFile csFile
            let args = if isTargetingDotNet45 then sprintf "%s /targetversion:4.5" args else args
            args
        
        Util.shell(csDir.Path, Util.edmGenExe(), edmGenArgs, Some formatErr)
        let assemblyFile = Util.TemporaryFile "dll" // TODO: load in-memory and clean up this file

        let cscCommand = sprintf "/nologo /resource:\"%s\",\"%s\"  /resource:\"%s\",\"%s\" /resource:\"%s\",\"%s\" /target:library /r:System.Data.Entity.dll /out:\"%s\" \"%s\"" csdlFile csdlFileName  mslFile mslFileName ssdlFile ssdlFileName assemblyFile.Path csFile

        Util.shell(csDir.Path, Util.cscExe(), cscCommand, None)
        assemblyFile

module internal SqlEntityConnection =

    let getSsdl (namespaceName, ssdlFile: Util.FileResource, provider, connString, flagPluralize, flagSuppressForeignKeyProperties, isTargetingDotNet45) = 
        let edmGenArgs = 
            let args = sprintf "/namespace:\"%s\" /project:\"%s\" /outssdl:\"%s\" /mode:FullGeneration  /connectionstring:\"%s\" " namespaceName (Path.GetFileNameWithoutExtension ssdlFile.Path) ssdlFile.Path connString 
            let args = if not (String.IsNullOrWhiteSpace provider)      then sprintf "%s /provider:%s" args provider else args
            let args = if flagPluralize                                 then sprintf "%s /pluralize" args else args
            let args = if flagSuppressForeignKeyProperties              then sprintf "%s /SuppressForeignKeyProperties" args else args
            let args = if isTargetingDotNet45                           then sprintf "%s /targetversion:4.5" args else args
            args
        Util.shell(System.IO.Path.GetDirectoryName ssdlFile.Path, Util.edmGenExe(), edmGenArgs, Some formatErr)
        File.ReadAllText ssdlFile.Path


    let computeEntityContainerName (entityContainer:string) =
        if String.IsNullOrWhiteSpace entityContainer then 
            "EntityContainer" 
        else 
            entityContainer 

    let buildType (namespaceName, connectionString, connectionStringName, configFileName, absoluteDataDirectory, localSchemaFile:string, provider:string, entityContainer:string, forceUpdate:bool, flagPluralize, flagSuppressForeignKeyProperties, designTimeDirectory, isTargetingDotNet45)  =
        let connectionString, usingConfigFileInfo = ConfigFiles.computeStaticConnectionString ("SqlEntityConnection", false, connectionString, connectionStringName, configFileName, absoluteDataDirectory, designTimeDirectory)
        let entityContainer = computeEntityContainerName entityContainer
        use ssdlFile = 
            tryUseLocalSchemaFile 
                (localSchemaFile, forceUpdate, "ssdl") 
                (fun ssdlFile -> 
                    try 
                        getSsdl (namespaceName, ssdlFile, provider, connectionString, flagPluralize, flagSuppressForeignKeyProperties, isTargetingDotNet45)
                    with 
                        e -> handleReadingSchemaError e)

        use csFile = Util.TemporaryFile "cs"
        use csdlFile = Util.TemporaryFile "csdl"
        use mslFile = Util.TemporaryFile "msl"
        use viewsFile = Util.TemporaryFile "Views.cs"

        let edmGenArgs = 
            let args = sprintf "/mode:FromSsdlGeneration /entitycontainer:\"%s\" /namespace:\"%s\"  /project:\"%s\" /inssdl:\"%s\" /outobjectlayer:\"%s\" /outcsdl:\"%s\" /outmsl:\"%s\" /outviews:\"%s\" /language:CSharp" entityContainer namespaceName (Path.GetFileNameWithoutExtension ssdlFile.Path) ssdlFile.Path  csFile.Path csdlFile.Path mslFile.Path viewsFile.Path
            let args = if not (String.IsNullOrWhiteSpace provider)      then sprintf "%s /provider:%s" args provider else args
            let args = if flagPluralize                                 then sprintf "%s /pluralize" args else args
            let args = if flagSuppressForeignKeyProperties              then sprintf "%s /SuppressForeignKeyProperties" args else args
            let args = if isTargetingDotNet45                           then sprintf "%s /targetversion:4.5" args else args
            args
        Util.shell(System.IO.Path.GetDirectoryName ssdlFile.Path, Util.edmGenExe(), edmGenArgs, Some formatErr)
        
        let assemblyFile = Util.TemporaryFile "dll" 

        let csdlFileName = namespaceName + ".csdl"
        let mslFileName = namespaceName + ".msl"
        let ssdlFileName = namespaceName + ".ssdl"

        let cscCommand = sprintf "/r:System.Data.Entity.dll /nologo /resource:\"%s\",\"%s\"  /resource:\"%s\",\"%s\" /resource:\"%s\",\"%s\" /nologo /target:library /out:\"%s\" \"%s\"" csdlFile.Path csdlFileName  mslFile.Path mslFileName ssdlFile.Path ssdlFileName assemblyFile.Path csFile.Path
        Util.shell(Path.GetTempPath(), Util.cscExe(), cscCommand, None) 
            
        assemblyFile, usingConfigFileInfo


