<scriptlet>
<implements type="Automation" id="dispatcher">
  <property name="PluginEvent">
    <get/>
  </property>
  <property name="PluginDescription">
    <get/>
  </property>
  <property name="PluginExtendedProperties">
    <get/>
  </property>
  <property name="PluginFileFilters">
    <get/>
  </property>
  <property name="PluginArguments">
    <get/>
    <put/>
  </property>
  <property name="PluginVariables">
    <get/>
    <put/>
  </property>
  <method name="PluginOnEvent"/>
  <method name="AIConvertText"/>
  <method name="ShowSettingsDialog"/>
</implements>

<script language="JScript">

var REGKEY_PATH = "Plugins\\AI.sct/";
var wsh = new ActiveXObject("WScript.Shell");
var fso = new ActiveXObject("Scripting.FileSystemObject");
var pluginArguments = "";
var variables = new Array();
var mergeApp;

function get_PluginEvent() { return "EDITOR_SCRIPT"; }
function get_PluginDescription() { return "AI-assisted text conversion"; }
function get_PluginFileFilters() { return ".*"; }
function get_PluginExtendedProperties() { return "GenerateUnpacker;AIConvertText.MenuCaption=Convert Text with AI...;AIConvertText.Description=Text converter (OpenAI API).\r\nUsage: AIConvertText PROMPT;AIConvertText.ArgumentsRequired;"; }
function get_PluginArguments() { return pluginArguments; }
function put_PluginArguments(NewValue) { pluginArguments = NewValue; }
function get_PluginVariables() { return variables.join("\0"); }
function put_PluginVariables(NewValue) { variables = NewValue.split("\0"); }
function regRead(Key, DefaultValue) { return mergeApp.GetOption(Key, DefaultValue); }
function regWrite(Key, Value, TypeNm) {
  mergeApp.SaveOption(Key, (TypeNm === "REG_DWORD") ? parseInt(Value, 10) : String(Value));
}

function ReplaceVariables(str) {
  var newstr = "";
  var pos = 0;
  while (true) {
    var foundpos = str.indexOf("%", pos);
    if (foundpos > 0) {
      var ch = str.charAt(foundpos + 1);
      if (ch === "%") {
        newstr = newstr + "%";
        pos = foundpos + 2;
      } else if (!isNaN(ch)) {
        newstr = newstr + str.substr(pos, foundpos - pos);
        if (parseInt(ch, 10) < variables.length) {
          newstr = newstr + variables(parseInt(ch, 10));
        }
        pos = foundpos + 2;
      } else {
        newstr = newstr + str.substr(pos, foundpos - pos + 1);
        pos = foundpos + 1;
      }
    } else {
      newstr = newstr + str.substr(pos);
      break;
    }
  }
  return newstr;
}

function IsFirstArgumentEmpty() { return pluginArguments.match(/^\s*$/) !== null; }

function Unescape(text) {
  var map = { 'a': String.fromCharCode(0x07), 'b': '\b', 't': '\t', 'n': '\n', 'v': String.fromCharCode(0x0b), 'f': '\f', 'r': '\r', '\"': '\"', '\\': '\\' };
  var result = "";
  var textLen = text.length;
  var i = 0;
  while (i < textLen) {
    var ch = text.charAt(i);
    if (ch === "\\") {
      if (i < textLen - 1) {
        i++;
        ch = text.charAt(i);
        var ch2 = map[ch];
        if (ch2 !== undefined) {
          result += ch2;
        } else if (ch == "x") {
          if (i + 2 < textLen) {
            try {
              result += String.fromCharCode(parseInt(text.substring(i + 1, i + 3), 16));
              i += 2;
            } catch (e) {
              result += "\\x";
            }
          } else {
            result += "\\x";
          }
        } else {
          if (!isNaN(ch)) {
            result += (ch !== '0') ? '$' + ch : '$&';
          } else {
            result += '\\' + ch;
          }
        }
      } else {
        result += ch;
      }
    } else {
      result += ch;
    }
    i++;
  }
  return result;
}

function PluginOnEvent(eventType, obj) { mergeApp = obj; }

function quote(text) {
  return text.replace(/\\/g, "\\\\").replace(/\"/g, "\\\"").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t")
}

function extractString(input) {
  var regex = /"((?:[^"\\]|\\.)*)"/g;
  var matches = [];
  var match;
  while ((match = regex.exec(input)) !== null)
    matches.push(match[1]);
  return matches;
}

function getContentOrThrowError(jsonString) {
  var extractedStrings = extractString(jsonString);
  for (var i = 0; i < extractedStrings.length; i++) {
    if (extractedStrings[i] === "content" && i + 1 < extractedStrings.length)
      return Unescape(extractedStrings[i + 1]);
  }
  throw new Error(30001, jsonString);
}

function setEnvVal(name, value) {
  wsh.Exec("setx " + name + " \"" + value + "\"");
}

function getOpenAIKey() {
  var keyEnvName = regRead(REGKEY_PATH + "OpenAI.ApiKeyEnvName", "OPENAI_API_KEY");
  var openAIKey = wsh.ExpandEnvironmentStrings("%" + keyEnvName + "%");
  if (openAIKey === "" || openAIKey === "%" + keyEnvName + "%") {
    try { openAIKey = wsh.RegRead("HKEY_CURRENT_USER\\Environment\\" + keyEnvName); } catch (e) { openAIKey = ""; }
    if (openAIKey === "") {
      var msg = mergeApp.Translate("Enter OpenAI API key.\r\n- Requires OpenAI registration (fees may apply).\r\n- Key stored in env var %1.");
      msg = msg.replace("%1", keyEnvName);
      openAIKey = mergeApp.InputBox(msg, "AIConvertText plugin", "");
      if (openAIKey !== "")
        setEnvVal(keyEnvName, openAIKey);
    }
  }
  return openAIKey;
}

function getTrailingNL(text) {
  var l = text.length;
  if (l === 0) return "";
  if (l >= 2 && text.substring(l - 2) === "\r\n") return "\r\n";
  var ch = text.substring(l - 1);
  return (ch === "\n" || ch === "\r") ? ch : "";
}

function syncTrailingNL(orgText, newText) {
  var orgNL = getTrailingNL(orgText);
  if (orgNL !== getTrailingNL(newText)) return newText.replace(/(\r\n|\r|\n)$/, '') + orgNL;
  return newText;
}

function getResp(xh) {
  var s = new ActiveXObject("ADODB.Stream");
  s.Type = 1; s.Open(); s.Write(xh.responseBody); s.Position = 0; s.Type = 2; s.Charset = "utf-8";
  var text = s.ReadText();
  s.Close();
  return text;
}

function AIConvertText(Text) {
  var openAIKey = getOpenAIKey();
  var cmd;
  if (IsFirstArgumentEmpty()) {
    cmd = regRead(REGKEY_PATH + "AIConvertText.Prompt", "");
    cmd = mergeApp.InputBox(mergeApp.Translate("Enter prompt"), "AIConvertText plugin", cmd);
    if (cmd !== "")
      regWrite(REGKEY_PATH + "AIConvertText.Prompt", cmd, "REG_SZ");
    cmd = ReplaceVariables(cmd);
  } else {
    cmd = pluginArguments;
  }
  if (cmd === "")
    throw new Error(30001, "Canceled");
  var openAIUrl = "https://api.openai.com/v1/chat/completions";
  var temperature = regRead(REGKEY_PATH + "OpenAI.Temperature", "1.00");
  var maxTokens = regRead(REGKEY_PATH + "OpenAI.MaxTokens", 4096);
  var model = regRead(REGKEY_PATH + "OpenAI.Model", "gpt-3.5-turbo");
  var body = '{"model": "' + model + '", "temperature": ' + Number(temperature) + ', "messages": [{"role": "system", "content": "' + quote(cmd) + '"}, {"role": "user", "content": "' + quote(Text) + '"}]}';
  var xh = new ActiveXObject("Msxml2.ServerXMLHTTP");
  xh.open("POST", openAIUrl, false);
  xh.setRequestHeader("Content-Type", "application/json");
  xh.setRequestHeader("Authorization", "Bearer " + openAIKey);
  xh.send(body);
  return syncTrailingNL(Text, getContentOrThrowError(getResp(xh)));
}

function translate(text) {
  var re = /\${([^}]+)}/g;
  var matches;
  while ((matches = re.exec(text)) != null) {
    text = text.replace(matches[0], mergeApp.Translate(matches[1]));
  }
  return text;
}

function ShowSettingsDialog() {
  var tname = fso.BuildPath(fso.GetSpecialFolder(2), fso.GetTempName() + ".hta");
  var xmlfile = fso.BuildPath(fso.GetSpecialFolder(2), fso.GetTempName() + ".xml");
  var tfile = fso.CreateTextFile(tname, true, true);
  tfile.Write(translate(getResource("dialog1")));
  tfile.Close();
  exportSettingsToXMLFile(xmlfile);
  var mshta = wsh.ExpandEnvironmentStrings("%SystemRoot%\\System32\\mshta.exe");
  if (!fso.FileExists(mshta)) {
    mshta = wsh.ExpandEnvironmentStrings("%SystemRoot%\\SysWOW64\\mshta.exe");
  }
  run(wsh, "\"" + mshta + "\" \"" + tname + "\"  \"" + xmlfile + "\"");
  importSettingsFromXMLFile(xmlfile);
  fso.DeleteFile(tname);
  fso.DeleteFile(xmlfile);
}

function run(sh, cmd) {
  sh.Run(cmd, 1, true);
}

function exportSettingsToXMLFile(filepath) {
  var key_defvalues = {
    "OpenAI.ApiKeyEnvName": "OPENAI_API_KEY",
    "OpenAI.Temperature": "1.00",
    "OpenAI.MaxTokens": 4096,
    "OpenAI.Model": "gpt-3.5-turbo"
  };
  var doc = new ActiveXObject("MSXML2.DOMDocument");
  var ts = fso.OpenTextFile(filepath, 2, true, -1);
  var root = doc.createElement("properties");
  for (var key in key_defvalues) {
    var el = doc.createElement("property");
    var val = regRead(REGKEY_PATH + key, key_defvalues[key]);
    var cdata = doc.createCDATASection(val);
    el.appendChild(cdata);
    el.setAttribute("name", REGKEY_PATH + key);
    el.setAttribute("type", typeof val);
    root.appendChild(el);
  }
  doc.appendChild(root);
  ts.Write(doc.xml);
  ts.Close();
}

function importSettingsFromXMLFile(filepath) {
  var ts = fso.OpenTextFile(filepath, 1, true, -1);
  var xml = ts.ReadAll();
  var doc = new ActiveXObject("MSXML2.DOMDocument");
  doc.async = false;
  doc.loadXML(xml);
  ts.Close();
  var nodes = doc.documentElement.childNodes;
  for (var i = 0; i < nodes.length; i++) {
    regWrite(nodes[i].getAttribute("name"), nodes[i].text, (nodes[i].getAttribute("type") === "string") ? "REG_SZ" : "REG_DWORD");
  }
}

</script>

<resource id="dialog1">
<![CDATA[
<!DOCTYPE html>
<html>
  <head>
    <HTA:APPLICATION ID="objHTA">
    <title>${AI.sct WinMerge Plugin Options}</title>
    <meta content="text/html" charset="UTF-16">
    <style>
    body { background-color: #f2f2f2; font-family: Arial, sans-serif; }
    .container { margin: 2em; }
    ul { list-style-type: none; margin: 0; padding: 0; }
    li ul li { padding-left: 2em }
    .btn-container { margin-top: 1.5em; text-align: right; }
    input[type="button"] { border: none; padding: 0.6em 2em; height: 2.5em; text-align: center; }
    .btn-ok { color: #fff; background-color: #05c; }
    .btn-ok:hover { background-color: #04b; }
    .btn-cancel { color: #333; background-color: #ddd; }
    .btn-cancel:hover { background-color: #ccc; }
    </style>
    <script type="text/javascript">
var REGKEY_PATH = "Plugins\\AI.sct/";
var xmlFilePath;
var settings = {};
var wsh = new ActiveXObject("WScript.Shell");
var fso = new ActiveXObject("Scripting.FileSystemObject");
var openAIKey = "";

function regRead(key, defaultValue) {
  return settings.hasOwnProperty(key) ? settings[key] : defaultValue;
}

function regWrite(key, value, type) {
  settings[key] = (type === "REG_DWORD") ? Number(value) : String(value);
}

function setEnvVal(name, value) {
  wsh.Exec("setx " + name + " \"" + value + "\"");
}

function loadSettingsFromXMLFile(filepath) {
  var ts = fso.OpenTextFile(filepath, 1, true, -1);
  var xml = ts.ReadAll();
  var doc = new ActiveXObject("MSXML2.DOMDocument");
  doc.async = false;
  doc.loadXML(xml);
  ts.Close();
  var nodes = doc.documentElement.childNodes;
  for (var i = 0; i < nodes.length; i++) {
    regWrite(nodes[i].getAttribute("name"), nodes[i].text, (nodes[i].getAttribute("type") === "string") ? "REG_SZ" : "REG_DWORD");
  }
  return settings;
}

function saveSettingsToXMLFile(filepath, settings) {
  var doc = new ActiveXObject("MSXML2.DOMDocument");
  var ts = fso.OpenTextFile(filepath, 2, true, -1);
  var root = doc.createElement("properties");
  for (var key in settings) {
    if (settings.hasOwnProperty(key)) {
      var el = doc.createElement("property");
      var val = settings[key];
      var cdata = doc.createCDATASection(val);
      el.appendChild(cdata);
      el.setAttribute("name", key);
      el.setAttribute("type", typeof val);
      root.appendChild(el);
    }
  }
  doc.appendChild(root);
  ts.Write(doc.xml);
  ts.Close();
}

function getOpenAIKey(keyEnvName) {
  var openAIKey = wsh.ExpandEnvironmentStrings("%" + keyEnvName + "%");
  if (openAIKey === "" || openAIKey === "%" + keyEnvName + "%") {
    try {
      openAIKey = wsh.RegRead("HKEY_CURRENT_USER\\Environment\\" + keyEnvName);
    } catch (e) {
      openAIKey = "";
    }
  }
  return openAIKey;
}

function run(sh, cmd) {
  sh.Run(cmd, 1, true);
}

function onload() {
  xmlFilePath = objHTA.commandLine.split('"')[3];
  settings = loadSettingsFromXMLFile(xmlFilePath);

  var dpi = window.screen.deviceXDPI;
  var w = 600 * dpi / 96, h = 500 * dpi / 96;
  window.resizeTo(w, h);
  window.moveTo((screen.width - w) / 2, (screen.height - h) / 2);

  var keyEnvName = regRead(REGKEY_PATH + "OpenAI.ApiKeyEnvName", "OPENAI_API_KEY");
  openAIKey = getOpenAIKey(keyEnvName);

  txtApiKeyEnvName.value = keyEnvName;
  txtApiKey.value = openAIKey;
  txtTemperature.value = regRead(REGKEY_PATH + "OpenAI.Temperature", "1.00");
  txtMaxTokens.value = regRead(REGKEY_PATH + "OpenAI.MaxTokens", 4096);
  cboModel.value = regRead(REGKEY_PATH + "OpenAI.Model", "gpt-3.5-turbo");
  document.onkeydown = onkeydown;
}

function onkeydown() {
  var k = event.keyCode;
  if (k == 13/*Enter*/) {
    btnOk_onclick();
  } else if (k == 27/*Escape*/) {
    btnCancel_onclick();
  }
}

function btnApiKey_onclick() {
  run(wsh, "cmd /c start \"\" https://platform.openai.com/api-keys");
}

function btnOk_onclick() {
  var temperature = Number(txtTemperature.value);
  var maxTokens = Number(txtMaxTokens.value);
  if (isNaN(temperature)) {
    temperature = 1.0;
  }
  if (temperature < 0.0) {
    temperature = 0.0;
  }
  if (temperature > 2.0) {
    temperature = 2.0;
  }
  if (isNaN(maxTokens)) {
    maxTokens = 4096;
  }
  if (cboModel.value === "gpt-3.5-turbo-16k") {
    if (maxTokens < 0 || maxTokens > 16384) {
      maxTokens = 16384;
    }
  } else {
    if (maxTokens < 0 || maxTokens > 4096) {
      maxTokens = 4096;
    }
  }
  regWrite(REGKEY_PATH + "OpenAI.ApiKeyEnvName", txtApiKeyEnvName.value, "REG_SZ");
  regWrite(REGKEY_PATH + "OpenAI.Temperature", temperature, "REG_SZ");
  regWrite(REGKEY_PATH + "OpenAI.MaxTokens", maxTokens, "REG_DWORD");
  regWrite(REGKEY_PATH + "OpenAI.Model", cboModel.value, "REG_SZ");
  if (txtApiKey.value !== openAIKey) {
    setEnvVal(txtApiKeyEnvName.value, txtApiKey.value);
    alert("${OpenAI API key changed. Restart to apply.}");
  }

  saveSettingsToXMLFile(xmlFilePath, settings);

  window.close();
}

function btnCancel_onclick() {
  saveSettingsToXMLFile(xmlFilePath, {});

  window.close();
}

    </script>
  </head>
  <body onload="onload();">
    <div class="container">
      <ul>
        <li>
          <label for="txtApiKeyEnvName">${Environment variable name for OpenAI API key}:</label>
        </li>
        <li>
          <input id="txtApiKeyEnvName" type="text" />
        </li>
        <li>
          <label for="txtApiKey"><a href="https://platform.openai.com/api-keys" onclick="btnApiKey_onclick(); return false;">${OpenAI API key}:</a></label>
        </li>
        <li>
          <input id="txtApiKey" type="password" />
        </li>
        <li>
          <label for="txtTemperature">${Temperature}:</label>
        </li>
        <li>
          <input id="txtTemperature" type="text" />
        </li>
        <li>
          <label for="txtMaxTokens">${Maximum length}:</label>
        <li>
          <input id="txtMaxTokens" type="text" />
        </li>
        <li>
          <label for="cboModel">${Model}:</label>
        </li>
        <li>
          <select id="cboModel">
            <option value="gpt-4.1-mini">gpt-4.1-mini</option>
            <option value="gpt-4.1-nano">gpt-4.1-nano</option>
            <option value="gpt-4.1">gpt-4.1</option>
            <option value="gpt-4o-mini">gpt-4o-mini</option>
            <option value="gpt-4o">gpt-4o</option>
            <option value="gpt-4-turbo">gpt-4-turbo</option>
            <option value="gpt-4">gpt-4</option>
            <option value="gpt-3.5-turbo">gpt-3.5-turbo</option>
            <option value="gpt-3.5-turbo-16k">gpt-3.5-turbo-16k</option>
          </select>
        </li>
      </ul>
      <div class="btn-container">
        <input type="button" class="btn-ok" onclick="btnOk_onclick();" value="${OK}" />
        <input type="button" class="btn-cancel" onclick="btnCancel_onclick();" value="${Cancel}" />
      </div>
    </div>
  </body>
</html>
]]>
</resource>

</scriptlet>
