/*
 * Copyright [2019] [Doric.Pub]
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import * as doric from './src/runtime/sandbox'
import WebSocket from "ws"
import path from 'path'
import { Panel } from './src/ui/panel';

type MSG = {
  type: "D2C" | "C2D" | "C2S" | "D2S" | "S2C" | "S2D",
  cmd: string,
  payload: { [index: string]: string | number | { type: number, value: string }[] }
}

let contextId: string | undefined = undefined;

let global = new Function('return this')()
const originSetTimeout = global.setTimeout
global.setTimeout = global.doricSetTimeout
global.setInterval = global.doricSetInterval
global.clearTimeout = global.doricClearTimeout
global.clearInterval = global.doricClearInterval
global.doric = doric


async function initNativeEnvironment(source: string) {
  // dev kit client
  return new Promise<string>((resolve, reject) => {
    const ws = new WebSocket('ws://localhost:7777')
      .on('open', () => {
        console.log('Connectted Devkit on port', '7777')
        ws.send(JSON.stringify({
          type: "D2C",
          cmd: "DEBUG_REQ",
          payload: {
            source,
          },
        } as MSG))
      })
      .on('message', (data) => {
        const msg = JSON.parse(data as string) as MSG
        const payload = msg.payload
        switch (msg.cmd) {
          case "DEBUG_RES":
            const contextId = msg.payload.contextId as string;
            resolve(contextId);
            break;
          case "injectGlobalJSObject":
            console.log("injectGlobalJSObject", payload);
            const type = payload.type as number
            const value = payload.value as string
            let arg
            if (type === 0) {
              arg = null
            } else if (type === 1) {
              arg = parseFloat(value)
            } else if (type === 2) {
              arg = (value == 'true')
            } else if (type === 3) {
              arg = value.toString()
            } else if (type === 4) {
              arg = JSON.parse(value)
            } else if (type === 5) {
              arg = JSON.parse(value)
            }
            if (payload.name === "Environment") {
              (arg as any).debugging = true
            }
            Reflect.set(global, payload.name as string, arg)
            break
          case "injectGlobalJSFunction":
            console.log("injectGlobalJSFunction", payload);
            if (payload.name === "nativeEmpty") {
              break
            }
            Reflect.set(global, payload.name as string, function () {
              let args = [].slice.call(arguments)
              console.log(args)
              console.log("injected", payload.name, args)
              ws.send(JSON.stringify({
                type: "D2C",
                cmd: 'injectGlobalJSFunction',
                payload: {
                  name: payload.name,
                  arguments: args
                }
              } as MSG))
            })
            break
          case "invokeMethod":
            const callId = payload.callId;
            console.log("invokeMethod", callId, payload)
            const values = payload.values as { type: number, value: string }[]
            let args = []
            for (let i = 0; i < values.length; i++) {
              let value = values[i]
              if (value.type === 0) {
                args.push(null)
              } else if (value.type === 1) {
                args.push(parseFloat(value.value))
              } else if (value.type === 2) {
                args.push((value.value == 'true'))
              } else if (value.type === 3) {
                args.push(value.value.toString())
              } else if (value.type === 4) {
                args.push(JSON.parse(value.value))
              } else if (value.type === 5) {
                args.push(JSON.parse(value.value))
              }
            }
            const object = Reflect.get(global, payload.objectName as string)
            const method = Reflect.get(object, payload.functionName as string)
            const result = Reflect.apply(method, undefined, args)
            console.log("Result", callId, result)
            ws.send(JSON.stringify({
              type: "D2C",
              cmd: 'invokeMethod',
              payload: {
                result,
                callId,
              }
            } as MSG))
            break;
          case "DEBUG_STOP":
            console.log(msg.payload?.msg || "Stop debugging");
            process.exit(0);
            break;
        }
      })
      .on('error', (error) => {
        console.log(error)
        reject(error)
      })
  })
}

global.context = doric.jsObtainContext("FakeContext");
const entryHooks: Function[] = []
global.Entry = function () {
  if (!!contextId) {
    return Reflect.apply(doric.jsObtainEntry(contextId), doric, arguments);
  } else {
    const jsFile = new Error().stack?.split("\n")
      .map(e => e.match(/at\s__decorate.*?\s\((.*?)\)/))
      .find(e => !!e)?.[1].match(/(.*?\.js)/)?.[1];
    if (!jsFile) {
      throw new Error("Cannot find debugging file");
    }
    const args = arguments
    entryHooks.push((contextId: string) => {
      Reflect.apply(doric.jsObtainEntry(contextId), doric, args);
    })
    if (entryHooks.length <= 1) {
      const source = path.basename(jsFile)
      console.log(`Debugging ${source}`)
      initNativeEnvironment(source).then(ret => {
        contextId = ret;
        console.log("debugging context id: " + contextId);
        const realContext = doric.jsObtainContext(contextId);
        global.context.id = contextId;
        global.context = realContext;
        entryHooks.forEach(e => e(contextId))
      });
      return arguments[0];
    }
  }
}

global.injectGlobal = (objName: string, obj: string) => {
  Reflect.set(global, objName, JSON.parse(obj))
}

global.sendToNative = () => {

}
global.receiveFromNative = () => {

}
global.nativeLog = (type: string, msg: string) => {
  switch (type) {
    case "w": {
      console.warn(msg);
      break;
    }
    case "e": {
      console.error(msg);
      break;
    }
    default: {
      console.log(msg);
      break;
    }
  }
}

global.nativeRequire = () => {
  console.error("nativeRequire", new Error().stack);
  console.error("Do not call here in debugging");
  return false;
}

global.nativeBridge = () => {
  console.error("nativeBridge", new Error().stack);
  console.error("Do not call here in debugging");
  return false;
}

global.Environment = new Proxy({}, {
  get: (target, p, receiver) => {
    console.error("Environment Getter", new Error().stack);
    console.error("Do not call here in debugging");
    return undefined
  },
  set: (target, p, v, receiver) => {
    console.error("Environment Setter", new Error().stack)
    console.error("Do not call here in debugging");
    return Reflect.set(target, p, v, receiver);
  }
})

global.nativeEmpty = () => {
  originSetTimeout(() => {
    for (let context of doric.allContexts()) {
      const entity = context.entity
      if (entity instanceof Panel) {
        const panel = entity as Panel
        if (panel.getRootView().isDirty()) {
          const model = panel.getRootView().toModel()
          context.callNative("shader", "render", model)
          panel.getRootView().clean()
        }
        for (let map of panel.allHeadViews()) {
          for (let v of map.values()) {
            if (v.isDirty()) {
              const model = v.toModel()
              context.callNative("shader", "render", model)
              v.clean()
            }
          }
        }
      }
    }

  }, 0)
}

export * from './index'