open System
open System.Collections.Generic
open System.Diagnostics
open System.IO
open System.Text.RegularExpressions
open System.Threading.Tasks

// "$(DevEnvDir)CommonExtensions\Microsoft\FSharp\fsi.exe" "$(ProjectDir)Resources\PostBuild.fsx" --exec --nologo -- "$(TargetDir)\" "$(ProjectDir)\" "$(ConfigurationName)"
// "$(ProjectDir)bld\post_build.exe" "$(TargetDir)\" "$(ProjectDir)\" "$(ConfigurationName)"

exception ArgumentUsage of string

#if !INTERACTIVE
[<EntryPoint>]
#endif
let main (argv: string[]) =
    try
        if argv.Length < 2 then
            #if INTERACTIVE
            raise (ArgumentUsage "fsi.exe PostBuild.fsx --exec --nologo -- <TargetDir> <ProjectDir> [ConfigurationName] [VersionTag]")
            #else
            raise (ArgumentUsage "PostBuild.exe <TargetDir> <ProjectDir> [ConfigurationName] [VersionTag]")
            #endif
            
        let _time name func =
            let sw = Stopwatch.StartNew()
            func()
            sw.Stop()
            printfn "[%s took %i ms]" name (int (Math.Round(sw.Elapsed.TotalMilliseconds)))
            
        let (+/) path1 path2 =
            Path.Combine(path1, path2)
            
        let sw = Stopwatch.StartNew()
        
        // Setup
        
        let targetDir = argv.[0]
        let projectDir = argv.[1]
        
        let configuration = if argv.Length >= 3 then argv.[2]
                            else "Release"
                            
        let version = if argv.Length >= 4 then argv.[3]
                      else ((targetDir +/ "TweetDuck.exe") |> FileVersionInfo.GetVersionInfo).FileVersion
                      
        printfn "--------------------------"
        printfn "TweetDuck version %s" version
        printfn "--------------------------"
        
        let localesDir = targetDir +/ "locales"
        let scriptsDir = targetDir +/ "scripts"
        let pluginsDir = targetDir +/ "plugins"
        let importsDir = scriptsDir +/ "imports"
        
        // Functions (Strings)
        
        let filterNotEmpty =
            Seq.filter (not << String.IsNullOrEmpty)
            
        let replaceRegex (pattern: string) (replacement: string) input =
            Regex.Replace(input, pattern, replacement)
            
        let collapseLines separator (sequence: string seq) =
            String.Join(separator, sequence)
            
        let trimStart (line: string) =
            line.TrimStart()
            
        // Functions (File Management)
        let copyFile source target =
            File.Copy(source, target, true)
            
        let createDirectory path =
            Directory.CreateDirectory(path) |> ignore
            
        let rec copyDirectoryContentsFiltered source target (filter: string -> bool) =
            if not (Directory.Exists(target)) then
                Directory.CreateDirectory(target) |> ignore
                
            let src = DirectoryInfo(source)
            
            for file in src.EnumerateFiles() do
                if filter file.Name then
                    file.CopyTo(target +/ file.Name) |> ignore
                    
            for dir in src.EnumerateDirectories() do
                if filter dir.Name then
                    copyDirectoryContentsFiltered dir.FullName (target +/ dir.Name) filter
                    
        let copyDirectoryContents source target =
            copyDirectoryContentsFiltered source target (fun _ -> true)
            
        // Functions (File Processing)
        
        let byPattern path pattern =
            Directory.EnumerateFiles(path, pattern, SearchOption.AllDirectories)
            
        let exceptEndingWith name =
            Seq.filter (fun (file: string) -> not (file.EndsWith(name)))
            
        let iterateFiles (files: string seq) (func: string -> unit) =
            Parallel.ForEach(files, func) |> ignore
            
        let readFile file = seq {
            use stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 0x1000, FileOptions.SequentialScan)
            use reader = new StreamReader(stream)
            let mutable cont = true
            
            while cont do
                let line = reader.ReadLine()
                
                if line = null then
                    cont <- false
                else
                    yield line
        }
        
        let writeFile (fullPath: string) (lines: string seq) =
            let relativePath = fullPath.[(targetDir.Length)..]
            let includeVersion = relativePath.StartsWith(@"scripts\") && not (relativePath.StartsWith(@"scripts\imports\"))
            let finalLines = if includeVersion then seq { yield "#" + version; yield! lines } else lines
            
            File.WriteAllLines(fullPath, finalLines |> filterNotEmpty |> Seq.toArray)
            printfn "Processed %s" relativePath
        
        let processFiles (files: string seq) (extProcessors: IDictionary<string, (string seq -> string seq)>) =
            let rec processFileContents file =
                readFile file
                |> extProcessors.[Path.GetExtension(file)]
                |> Seq.map (fun line ->
                    Regex.Replace(line, @"#import ""(.*?)""", (fun matchInfo ->
                        processFileContents(importsDir +/ matchInfo.Groups.[1].Value.Trim())
                        |> collapseLines (Environment.NewLine)
                        |> (fun contents -> contents.TrimEnd())
                    ))
                )
                
            iterateFiles files (fun file ->
                processFileContents file
                |> (writeFile file)
            )
            
        // Build
            
        copyFile (projectDir +/ "bld/Resources/LICENSES.txt") (targetDir +/ "LICENSES.txt")
        
        copyDirectoryContents (projectDir +/ "Resources/Scripts") scriptsDir
        
        createDirectory (pluginsDir +/ "official")
        createDirectory (pluginsDir +/ "user")
        
        copyDirectoryContentsFiltered
            (projectDir +/ "Resources/Plugins")
            (pluginsDir +/ "official")
            (fun name -> name <> ".debug" && name <> "emoji-instructions.txt")
            
        if configuration = "Debug" then
            copyDirectoryContents
                (projectDir +/ "Resources/Plugins/.debug")
                (pluginsDir +/ "user/.debug")
                
        if Directory.Exists(localesDir) || configuration = "Release" then
            Directory.EnumerateFiles(localesDir, "*.pak")
            |> exceptEndingWith @"\en-US.pak"
            |> Seq.iter (File.Delete)
            
        // Validation
        
        if File.ReadAllText(pluginsDir +/ "official/emoji-keyboard/emoji-ordering.txt").IndexOf('\r') <> -1 then
            raise (FormatException("emoji-ordering.txt must not have any carriage return characters"))
        else
            printfn "Verified emoji-ordering.txt"
            
        // Processing
        
        let fileProcessors =
            dict [
                ".js", (fun (lines: string seq) ->
                    lines
                    |> Seq.map (fun line ->
                        line
                        |> trimStart
                        |> replaceRegex @"^(.*?)((?<=^|[;{}()])\s?//(?:\s.*|$))?$" "$1"
                        |> replaceRegex @"(?<!\w)(return|throw)(\s.*?)? if (.*?);" "if ($3)$1$2;"
                    )
                );
                
                ".css", (fun (lines: string seq) ->
                    lines
                    |> Seq.map (fun line ->
                        line
                        |> replaceRegex @"\s*/\*.*?\*/" ""
                        |> replaceRegex @"^(\S.*) {$" "$1{"
                        |> replaceRegex @"^\s+(.+?):\s*(.+?)(?:\s*(!important))?;$" "$1:$2$3;"
                    )
                    |> filterNotEmpty
                    |> collapseLines " "
                    |> replaceRegex @"([{};])\s" "$1"
                    |> replaceRegex @";}" "}"
                    |> Seq.singleton
                );
                
                ".html", (fun (lines: string seq) ->
                    lines
                    |> Seq.map trimStart
                );
                
                ".meta", (fun (lines: string seq) ->
                    lines
                    |> Seq.map (fun line -> line.Replace("{version}", version))
                );
            ]
            
        processFiles ((byPattern targetDir "*.js") |> exceptEndingWith @"\configuration.default.js") fileProcessors
        processFiles (byPattern targetDir "*.css") fileProcessors
        processFiles (byPattern targetDir "*.html") fileProcessors
        processFiles (byPattern pluginsDir "*.meta") fileProcessors
        
        // Cleanup
        
        Directory.Delete(importsDir, true)
        
        // Finished
        
        sw.Stop()
        printfn "------------------"
        printfn "Finished in %i ms" (int (Math.Round(sw.Elapsed.TotalMilliseconds)))
        printfn "------------------"
        0
        
    with
    | ArgumentUsage message ->
        printfn ""
        printfn "Build script usage:"
        printfn "%s" message
        printfn ""
        1
    | ex ->
        printfn ""
        printfn "Encountered an error while running PostBuild:"
        printfn "%A" ex
        printfn ""
        1
        
#if INTERACTIVE
printfn "Running PostBuild in interpreter..."
main (fsi.CommandLineArgs |> Array.skip (1 + (fsi.CommandLineArgs |> Array.findIndex (fun arg -> arg = "--"))))
#endif