/*

 * Copyright (c) 2021-2022 Huawei Device Co., Ltd.

 * 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.

 */



const WebSocket = require('ws');

const ts = require('typescript');

const path = require('path');

const fs = require('fs');

const { spawn, execSync } = require('child_process');

const _ = require('lodash');



const { processComponentChild } = require('../lib/process_component_build');

const { createWatchCompilerHost } = require('../lib/ets_checker');

const { writeFileSync } = require('../lib/utils');

const { projectConfig } = require('../main');

const { props } = require('../lib/ets_checker');

const {

  isResource,

  processResourceData,

  isAnimateToOrImmediately,

  processAnimateToOrImmediately

} = require('../lib/process_ui_syntax');

const { dollarCollection } = require('../lib/ets_checker');



const WebSocketServer = WebSocket.Server;



let pluginSocket = '';



let supplement = {

  isAcceleratePreview: false,

  line: 0,

  column: 0,

  fileName: ''

};



const pluginCommandChannelMessageHandlers = {

  'compileComponent': handlePluginCompileComponent,

  'default': () => {}

};

let es2abcFilePath = path.join(__dirname, '../bin/ark/build-win/bin/es2abc');



let previewCacheFilePath;

let previewJsFilePath;

let previewAbcFilePath;

const messages = [];

let start = false;

let checkStatus = false;

let compileStatus = false;

let receivedMsg_;

let errorInfo = [];

let compileWithCheck;

let globalVariable = [];

let propertyVariable = [];

let globalDeclaration = new Map();

let connectNum = 0;

const maxConnectNum = 8;

let codeOverMaxlength = false;



let callback = undefined;



function buildPipeServer() {

  return {

    init(cachePath, buildPath, cb) {

      previewCacheFilePath = path.join(cachePath || buildPath, 'preview.ets');

      const rootFileNames = [];

      writeFileSync(previewCacheFilePath, '');

      rootFileNames.push(previewCacheFilePath);

      ts.createWatchProgram(

        createWatchCompilerHost(rootFileNames, resolveDiagnostic, delayPrintLogCount, ()=>{}, true));

      callback = cb;

    },

    compileComponent(jsonData) {

      handlePluginCompileComponent(jsonData);

    }

  };

}



function init(port) {

  previewCacheFilePath =

    path.join(projectConfig.cachePath || projectConfig.buildPath, 'preview.ets');

  previewJsFilePath =

    path.join(projectConfig.cachePath || projectConfig.buildPath, 'preview.js');

  previewAbcFilePath =

    path.join(projectConfig.cachePath || projectConfig.buildPath, 'preview.abc');

  const rootFileNames = [];

  writeFileSync(previewCacheFilePath, '');

  rootFileNames.push(previewCacheFilePath);

  ts.createWatchProgram(

    createWatchCompilerHost(rootFileNames, resolveDiagnostic, delayPrintLogCount, ()=>{}, true));

  const wss = new WebSocketServer({

    port: port,

    host: '127.0.0.1'

  });

  wss.on('connection', function(ws) {

    if (connectNum < maxConnectNum) {

      connectNum++;

      handlePluginConnect(ws);

    } else {

      ws.terminate();

    }

  });

}



function handlePluginConnect(ws) {

  ws.on('message', function(message) {

    pluginSocket = ws;

    try {

      const jsonData = JSON.parse(message);

      handlePluginCommand(jsonData);

    } catch (e) {

    }

  });

}



function handlePluginCommand(jsonData) {

  pluginCommandChannelMessageHandlers[jsonData.command] ?

    pluginCommandChannelMessageHandlers[jsonData.command](jsonData) :

    pluginCommandChannelMessageHandlers['default'](jsonData);

}



function handlePluginCompileComponent(jsonData) {

  if (jsonData) {

    messages.push(jsonData);

    if (receivedMsg_) {

      return;

    }

  } else if (messages.length > 0) {

    jsonData = messages[0];

  } else {

    return;

  }

  start = true;

  const receivedMsg = _.cloneDeep(jsonData);

  const compilerOptions = ts.readConfigFile(

    path.resolve(__dirname, '../tsconfig.json'), ts.sys.readFile).config.compilerOptions;

  Object.assign(compilerOptions, {

    'sourceMap': false

  });

  const sourceNode = ts.createSourceFile('preview.ets',

    'struct preview{build(){' + receivedMsg.data.script.replace(/new\s+\b(Circle|Ellipse|Rect|Path)\b/g,

      (item, item1) => {

        return 'special' + item1 + 'ForPreview';

      }) + '}}',

    ts.ScriptTarget.Latest, true, ts.ScriptKind.ETS, compilerOptions);

  compileWithCheck = jsonData.data.compileWithCheck || 'true';

  receivedMsg.data.variableScript = '';

  checkPreparation(receivedMsg, sourceNode);

  const previewStatements = [];

  const log = [];

  supplement = {

    isAcceleratePreview: true,

    line: parseInt(JSON.parse(receivedMsg.data.offset).line),

    column: parseInt(JSON.parse(receivedMsg.data.offset).column),

    fileName: receivedMsg.data.filePath || ''

  };

  try {

    processComponentChild(sourceNode.statements[0].members[1].body, previewStatements, log, supplement);

  } catch (e) {

    log.push({

      message: e.stack

    });

  }

  supplement.isAcceleratePreview = false;

  const newSource = ts.factory.updateSourceFile(sourceNode, previewStatements);

  const transformedSourceFile = transformResourceNode(newSource, log);

  const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });

  const result = printer.printNode(ts.EmitHint.Unspecified, transformedSourceFile, transformedSourceFile);

  receivedMsg.data.script = ts.transpileModule(result, {}).outputText.replace(

    /\bspecial(Circle|Ellipse|Rect|Path)ForPreview\b/g, (item, item1) => {

      return 'new ' + item1;

    });

  receivedMsg.data.log = log;

  if (receivedMsg.data.viewID) {

    receivedMsg.data.script = receivedMsg.data.variableScript + `function quickPreview(context) {

      const fastPreview = function build(){

        ${receivedMsg.data.script}

      }.bind(context);

      fastPreview();

    }

    quickPreview(GetRootView().findChildByIdForPreview(${receivedMsg.data.viewID}))`;

  }

  callEs2abc(receivedMsg);

}



function transformResourceNode(newSource, log) {

  const transformerFactory = (context) => {

    return (rootNode) => {

      function visit(node) {

        node = ts.visitEachChild(node, visit, context);

        return processResourceNode(node, log);

      }

      return ts.visitNode(rootNode, visit);

    };

  };

  const transformationResult = ts.transform(newSource, [transformerFactory]);

  return transformationResult.transformed[0];

}



function processResourceNode(node, log) {

  if (isResource(node)) {

    return processResourceData(node, '', {isAcceleratePreview: true, log: log});

  } else if (isAnimateToOrImmediately(node)) {

    return processAnimateToOrImmediately(node);

  } else {

    return node;

  }

}



function checkPreparation(receivedMsg, sourceNode) {

  let variableScript = '';

  if (previewCacheFilePath && fs.existsSync(previewCacheFilePath) && compileWithCheck === 'true') {

    globalVariable = receivedMsg.data.globalVariable || globalVariable;

    globalVariable = globalVariable.map((item) => {

      globalDeclaration.set(item.identifier, item.declaration);

      return item.identifier;

    });

    for (const [key, value] of sourceNode.identifiers) {

      if (globalVariable.includes(key)) {

        variableScript += globalDeclaration.get(key) + '\n';

      } else if (key.startsWith('$$') && globalVariable.includes(key.substring(2))) {

        variableScript += globalDeclaration.get(key.substring(2)) + '\n';

      }

    }

    propertyVariable = receivedMsg.data.propertyVariable || propertyVariable;

    receivedMsg.data.variableScript = ts.transpileModule(variableScript, {}).outputText;

    writeFileSync(previewCacheFilePath, variableScript + 'struct preview{build(){' + receivedMsg.data.script + '}}');

  }

}



function callEs2abc(receivedMsg) {

  if (fs.existsSync(es2abcFilePath + '.exe') || fs.existsSync(es2abcFilePath)) {

    es2abc(receivedMsg);

  } else {

    es2abcFilePath = path.join(__dirname, '../bin/ark/build-mac/bin/es2abc');

    if (fs.existsSync(es2abcFilePath)) {

      es2abc(receivedMsg);

    }

  }

}



function es2abc(receivedMsg) {

  try {

    const transCode = spawn(es2abcFilePath,

      ['--base64Input', Buffer.from(receivedMsg.data.script).toString('base64'), '--base64Output'], {windowsHide: true});

    transCode.stdout.on('data', (data) => {

      receivedMsg.data.script = data.toString();

      nextResponse(receivedMsg);

    });

    transCode.stderr.on('data', (data) => {

      receivedMsg.data.script = '';

      nextResponse(receivedMsg);

    });

  } catch (e) {

    if (checkStatus) {

      getOverLengthCode(receivedMsg);

    } else {

      codeOverMaxlength = true;

      receivedMsg_ = receivedMsg;

    }

  }

}



function getOverLengthCode(receivedMsg) {

  writeFileSync(previewJsFilePath, receivedMsg.data.script);

  const cmd = '"' + es2abcFilePath + '" ' + previewJsFilePath + ' --output ' + previewAbcFilePath;

  execSync(cmd, {windowsHide: true});

  if (fs.existsSync(previewAbcFilePath)) {

    receivedMsg.data.script = fs.readFileSync(previewAbcFilePath).toString('base64');

  } else {

    receivedMsg.data.script = '';

  }

  nextResponse(receivedMsg);

}



function nextResponse(receivedMsg) {

  compileStatus = true;

  receivedMsg_ = receivedMsg;

  responseToPlugin();

}



function resolveDiagnostic(diagnostic) {

  const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');

  if (validateError(message)) {

    if (diagnostic.file) {

      const { line, character } =

        diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);

      errorInfo.push(

        `ArkTS:ERROR File: ${diagnostic.file.fileName}:${line + 1}:${character + 1}\n ${message}\n`);

    } else {

      errorInfo.push(`ArkTS:ERROR: ${message}`);

    }

  }

}



function delayPrintLogCount() {

  if (start == true) {

    checkStatus = true;

    if (codeOverMaxlength && !errorInfo.length && !receivedMsg_.data.log.length) {

      getOverLengthCode(receivedMsg_);

    } else {

      if (codeOverMaxlength) {

        compileStatus = true;

      }

      responseToPlugin();

    }

  }

}



function responseToPlugin() {

  if ((compileWithCheck !== 'true' && compileStatus == true) ||

    (compileWithCheck === 'true' && compileStatus == true && checkStatus == true)) {

    if (receivedMsg_) {

      if (errorInfo && errorInfo.length) {

        receivedMsg_.data.log =  receivedMsg_.data.log || [];

        receivedMsg_.data.log.push(...errorInfo);

      }

      if (callback) {

        callback(JSON.stringify(receivedMsg_));

        afterResponse();

      } else {

        pluginSocket.send(JSON.stringify(receivedMsg_), (err) => {

          afterResponse();

        });

      }

    }

  }

}



function afterResponse() {

  start = false;

  checkStatus = false;

  compileStatus = false;

  codeOverMaxlength = false;

  errorInfo = [];

  receivedMsg_ = undefined;

  globalDeclaration.clear();

  messages.shift();

  if (messages.length > 0) {

    handlePluginCompileComponent();

  }

}



function validateError(message) {

  const stateInfoReg = /Property\s*'(\$?[_a-zA-Z0-9]+)' does not exist on type/;

  const $$InfoReg = /Cannot find name\s*'(\$\$[_a-zA-Z0-9]+)'/;

  if (matchMessage(message, [...propertyVariable, ...props], stateInfoReg) ||

    matchMessage(message, [...dollarCollection], $$InfoReg)) {

    return false;

  }

  return true;

}



function matchMessage(message, nameArr, reg) {

  if (reg.test(message)) {

    const match = message.match(reg);

    if (match[1] && nameArr.includes(match[1])) {

      return true;

    }

  }

  return false;

}



module.exports = {

  init,

  buildPipeServer

};