/// <reference path="../../../typings/index.d.ts" />
import * as express from "express"
import * as Settings from "./settings"
import * as urlModule from "url"
import * as pathModule from "path"
import { Context } from "../../../cli/lib/context"
import * as Leaf from "../../../build/leaf.export"
import { TSConfigParser } from "../../../cli/lib/tsConfigParser"
import * as fs from "fs"
import * as child_process from "child_process"
import * as wrench from "wrench"
import * as ws from "ws"
import * as http from "http"
import * as chokidar from "chokidar"
import * as colors from "colors"
const findFreePort = require("find-free-port") as Function
if (Settings.debug) {
    console.log("Debug mode: On")
}

abstract class ComposerServerDesign {
    constructor(public parser: TSConfigParser) { }
    abstract start(port: string | number);
}
export class ComposerServer extends ComposerServerDesign {
    public app: express.Application
    public server: http.Server
    public wsServer: ws.Server
    public context: Context
    public port: string | number = 3780
    public RCache: string
    public RJSCache: string
    public stylePath = "/style/main.css"
    public styleFilePath: string = "/style/main.css"
    public scriptPath = "/script/R.js"
    public rpcManager = new RPCManager()
    public setStylePath(path: string) {
        console.log("set style path")
        this.styleFilePath = path
        this.stylePath = pathModule.join("/", path);
        this.applyWatcher()
    }
    constructor(parser: TSConfigParser) {
        super(parser)
        this.app = express()
        this.server = http.createServer(this.app)
        this.wsServer = new ws.Server({ server: this.server })
        this.wsServer.on("connection", (con) => {
            let service = new RPCService()
            let adapter = new RPCWSAdapter(con)
            service.setConnection(adapter)
            this.rpcManager.add(service)
            adapter.events.listenBy(this, "close", () => {
                this.rpcManager.remove(service)
                adapter.events.stopListenBy(this)
            })
        })
        this.app.get(this.scriptPath, (req, res, next) => {
            let context = this.createContext()
            let result: string;
            try {
                result = context.compile()
            } catch (e) {
                console.error("compile error", e)
                if (this.RJSCache) {
                    res.end(this.RJSCache)
                } else {
                    res.end("console.error('Compile error')")
                }
            }
            if (result != this.RCache) {
                fs.writeFileSync(pathModule.join(Settings.tempFolder, "R.ts"), result)
                fs.writeFileSync(pathModule.join(Settings.tempFolder, "tsconfig.json"), JSON.stringify({
                    "compilerOptions": {
                        "target": "ES5",
                        "sourceMap": false,
                        "module": "commonjs"
                    }
                }))
                try {
                    console.log(child_process.execSync("tsc -p " + pathModule.join(Settings.tempFolder, "tsconfig.json")))
                } catch (e) {
                    console.error(colors.red("Fail to compile R"), e.stdout.toString())
                }
                let content = fs.readFileSync(pathModule.join(Settings.tempFolder, "R.js"), "utf8")
                this.RCache = result
                this.RJSCache = content
                res.end(content)
            } else {
                res.end(this.RJSCache)
            }
        })
        this.app.get("/", (req, res, next) => {
            let html = fs.readFileSync(pathModule.join(Settings.staticPath, "index.html"), "utf8")
            html = html.replace("{{stylePath}}", this.stylePath).replace("{{scriptPath}}", this.scriptPath)
            res.end(html)
        })
        this.app.get("/index.html", (req, res, next) => {
            res.redirect("/")
        })
        console.error(this.parser.getRootPath(), colors.green("run at"))
        this.app.use("/", express.static(this.parser.getRootPath()))
        this.app.use("/composer", express.static(Settings.staticPath))
        this.applyWatcher()
    }
    public tsWatcher: chokidar.FSWatcher
    public styleWatcher: chokidar.FSWatcher
    public applyWatcher() {
        if (this.tsWatcher) {
            this.tsWatcher.close()
            this.styleWatcher.close()
        }
        this.tsWatcher = chokidar.watch(this.parser.getTemplatePath())
        this.styleWatcher = chokidar.watch(pathModule.join(this.parser.getRootPath(), this.styleFilePath))
        let change = (filename) => {
            this.fireChangeEvent()
        }
        this.tsWatcher.on("add", change)
            .on("change", change)
        this.styleWatcher.on("add", change)
            .on("change", change)
    }
    public createContext() {
        let context = new Context(this.parser.getTemplatePath())
        context.rootName = "TestR"
        let files = wrench.readdirSyncRecursive(this.parser.getTemplatePath()).filter((item) => {
            try {
                return fs.statSync(pathModule.join(this.parser.getTemplatePath(), item)).isFile()
            } catch (e) {
                return false
            }
        })
        for (let file of files) {
            context.includeWidgetByRelativeFilePath(file)
        }
        return context
    }
    public start(port: string | number) {
        this.port = port
        findFreePort(parseInt(this.port.toString()), (err, port) => {
            if (err) {
                console.error(colors.red("fail to find any available port"));
                return
            }
            this.server.listen(parseInt(port.toString()), Settings.host, () => {
                this.port = port
                console.log(colors.green(`Composer server listen at http://0.0.0.0:${port}`))
            })
        })
    }
    private fireTimer = null
    private changeTimerDebounce: number = 300
    public fireChangeEvent() {
        clearTimeout(this.fireTimer)
        this.fireTimer = setTimeout(() => {
            for (let rpc of this.rpcManager.rpcs) {
                rpc.emitRemoteEvent("change")
            }
        }, this.changeTimerDebounce)
    }
}
class RPCManager {
    rpcs: RPCService[] = []
    constructor() {
    }
    add(rpc: RPCService) {
        this.rpcs.push(rpc)
    }
    remove(rpc: RPCService) {
        this.rpcs = this.rpcs.filter(item => item !== rpc)
    }
}
class RPCService extends Leaf.RPCComposer {
}
class RPCWSAdapter extends Leaf.RPCConnectionBase {
    constructor(private con: ws) {
        super()
        this.con.once("close", () => {
            this.events.emit("close")
        })
    }
    send(message: string) {
        this.con.send(message)
    }
}
