/* Copyright (C) 2016-2019 Brian P. Hinz
 *
 * This is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this software; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
 * USA.
 */

package com.tigervnc.vncviewer;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.PrintWriter;
import java.util.StringTokenizer;

import com.tigervnc.rfb.*;
import com.tigervnc.rfb.Exception;

public class Parameters {

  public static BoolParameter noLionFS
  = new BoolParameter("NoLionFS",
    "On Mac systems, setting this parameter will force the use of the old "+
    "(pre-Lion) full-screen mode, even if the viewer is running on OS X 10.7 "+
    "Lion or later.",
    false);

  public static BoolParameter dotWhenNoCursor
  = new BoolParameter("DotWhenNoCursor",
    "[DEPRECATED] Show the dot cursor when the server sends an invisible cursor",
    false);

  public static BoolParameter alwaysCursor
  = new BoolParameter("AlwaysCursor",
    "Show the local cursor when the server sends an invisible cursor",
    false);

  public static StringParameter cursorType
  = new StringParameter("CursorType",
    "Specify which cursor type the local cursor should be. Should be either Dot or System",
    "Dot");

  public static BoolParameter sendLocalUsername
  = new BoolParameter("SendLocalUsername",
    "Send the local username for SecurityTypes "+
    "such as Plain rather than prompting",
    true);

  public static StringParameter passwordFile
  = new StringParameter("PasswordFile",
    "Password file for VNC authentication",
    "");

  public static AliasParameter passwd
  = new AliasParameter("passwd",
    "Alias for PasswordFile",
    passwordFile);

  public static BoolParameter autoSelect
  = new BoolParameter("AutoSelect",
    "Auto select pixel format and encoding",
    true);

  public static BoolParameter fullColor
  = new BoolParameter("FullColor",
    "Use full color - otherwise 6-bit colour is used "+
    "until AutoSelect decides the link is fast enough",
    true);

  public static AliasParameter fullColorAlias
  = new AliasParameter("FullColour",
    "Alias for FullColor",
    Parameters.fullColor);

  public static IntParameter lowColorLevel
  = new IntParameter("LowColorLevel",
    "Color level to use on slow connections. "+
    "0 = Very Low, 1 = Low, 2 = Medium",
    2);

  public static AliasParameter lowColorLevelAlias
  = new AliasParameter("LowColourLevel",
    "Alias for LowColorLevel",
    lowColorLevel);

  public static StringParameter preferredEncoding
  = new StringParameter("PreferredEncoding",
    "Preferred encoding to use (Tight, ZRLE, "+
    "hextile or raw) - implies AutoSelect=0",
    "Tight");

  public static BoolParameter remoteResize
  = new BoolParameter("RemoteResize",
    "Dynamically resize the remote desktop size as "+
    "the size of the local client window changes. "+
    "(Does not work with all servers)",
    true);

  public static BoolParameter viewOnly
  = new BoolParameter("ViewOnly",
    "Don't send any mouse or keyboard events to the server",
    false);

  public static BoolParameter shared
  = new BoolParameter("Shared",
    "Don't disconnect other viewers upon "+
    "connection - share the desktop instead",
    false);

  public static BoolParameter maximize
  = new BoolParameter("Maximize",
    "Maximize viewer window",
    false);

  public static BoolParameter fullScreen
  = new BoolParameter("FullScreen",
    "Enable full screen",
    false);

  public static BoolParameter fullScreenAllMonitors
  = new BoolParameter("FullScreenAllMonitors",
    "Enable full screen over all monitors",
    true);

  public static BoolParameter acceptClipboard
  = new BoolParameter("AcceptClipboard",
    "Accept clipboard changes from the server",
    true);

  public static BoolParameter sendClipboard
  = new BoolParameter("SendClipboard",
    "Send clipboard changes to the server",
    true);

  public static IntParameter maxCutText
  = new IntParameter("MaxCutText",
    "Maximum permitted length of an outgoing clipboard update",
    262144);

  public static StringParameter menuKey
  = new StringParameter("MenuKey",
    "The key which brings up the popup menu",
    "F8");

  public static StringParameter desktopSize
  = new StringParameter("DesktopSize",
    "Reconfigure desktop size on the server on connect (if possible)",
    "");

  public static BoolParameter listenMode
  = new BoolParameter("listen",
    "Listen for connections from VNC servers",
    false);

  public static StringParameter scalingFactor
  = new StringParameter("ScalingFactor",
    "Reduce or enlarge the remote desktop image. "+
    "The value is interpreted as a scaling factor "+
    "in percent. If the parameter is set to "+
    "\"Auto\", then automatic scaling is "+
    "performed. Auto-scaling tries to choose a "+
    "scaling factor in such a way that the whole "+
    "remote desktop will fit on the local screen. "+
    "If the parameter is set to \"FixedRatio\", "+
    "then automatic scaling is performed, but the "+
    "original aspect ratio is preserved.",
    "100");

  public static BoolParameter alwaysShowServerDialog
  = new BoolParameter("AlwaysShowServerDialog",
    "Always show the server dialog even if a server has been "+
    "specified in an applet parameter or on the command line",
    false);

  public static BoolParameter acceptBell
  = new BoolParameter("AcceptBell",
    "Produce a system beep when requested to by the server.",
    true);

  public static StringParameter via
  = new StringParameter("Via",
    "Automatically create an encrypted TCP tunnel to "+
    "the gateway machine, then connect to the VNC host "+
    "through that tunnel. By default, this option invokes "+
    "SSH local port forwarding using the embedded JSch "+
    "client, however an external SSH client may be specified "+
    "using the \"-extSSH\" parameter. Note that when using "+
    "the -via option, the VNC host machine name should be "+
    "specified from the point of view of the gateway machine, "+
    "e.g. \"localhost\" denotes the gateway, "+
    "not the machine on which the viewer was launched. "+
    "See the System properties section below for "+
    "information on configuring the -Via option.", "");

  public static BoolParameter tunnel
  = new BoolParameter("Tunnel",
    "The -Tunnel command is basically a shorthand for the "+
    "-via command when the VNC server and SSH gateway are "+
    "one and the same. -Tunnel creates an SSH connection "+
    "to the server and forwards the VNC through the tunnel "+
    "without the need to specify anything else.", false);

  public static BoolParameter extSSH
  = new BoolParameter("extSSH",
    "By default, SSH tunneling uses the embedded JSch client "+
    "for tunnel creation. This option causes the client to "+
    "invoke an external SSH client application for all tunneling "+
    "operations. By default, \"/usr/bin/ssh\" is used, however "+
    "the path to the external application may be specified using "+
    "the -SSHClient option.", false);

  public static StringParameter extSSHClient
  = new StringParameter("extSSHClient",
    "Specifies the path to an external SSH client application "+
    "that is to be used for tunneling operations when the -extSSH "+
    "option is in effect.", "/usr/bin/ssh");

  public static StringParameter extSSHArgs
  = new StringParameter("extSSHArgs",
    "Specifies the arguments string or command template to be used "+
    "by the external SSH client application when the -extSSH option "+
    "is in effect. The string will be processed according to the same "+
    "pattern substitution rules as the VNC_TUNNEL_CMD and VNC_VIA_CMD "+
    "system properties, and can be used to override those in a more "+
    "command-line friendly way. If not specified, then the appropriate "+
    "VNC_TUNNEL_CMD or VNC_VIA_CMD command template will be used.", "");

  public static StringParameter sshConfig
  = new StringParameter("SSHConfig",
    "Specifies the path to an OpenSSH configuration file that to "+
    "be parsed by the embedded JSch SSH client during tunneling "+
    "operations.", FileUtils.getHomeDir()+".ssh/config");

  public static StringParameter sshKey
  = new StringParameter("SSHKey",
    "When using the Via or Tunnel options with the embedded SSH client, "+
    "this parameter specifies the text of the SSH private key to use when "+
    "authenticating with the SSH server. You can use \\n within the string "+
    "to specify a new line.", "");

  public static StringParameter sshKeyFile
  = new StringParameter("SSHKeyFile",
    "When using the Via or Tunnel options with the embedded SSH client, "+
    "this parameter specifies a file that contains an SSH private key "+
    "(or keys) to use when authenticating with the SSH server. If not "+
    "specified, ~/.ssh/id_dsa or ~/.ssh/id_rsa will be used (if they exist). "+
    "Otherwise, the client will fallback to prompting for an SSH password.",
    "");

  public static StringParameter sshKeyPass
  = new StringParameter("SSHKeyPass",
    "When using the Via or Tunnel options with the embedded SSH client, "+
    "this parameter specifies the passphrase for the SSH key.", "");

  public static BoolParameter customCompressLevel
  = new BoolParameter("CustomCompressLevel",
    "Use custom compression level. Default if CompressLevel is specified.",
    false);

  public static IntParameter compressLevel
  = new IntParameter("CompressLevel",
    "Use specified lossless compression level. 0 = Low, 9 = High. Default is 2.",
    2);

  public static BoolParameter noJpeg
  = new BoolParameter("NoJPEG",
    "Disable lossy JPEG compression in Tight encoding.",
    false);

  public static IntParameter qualityLevel
  = new IntParameter("QualityLevel",
    "JPEG quality level. 0 = Low, 9 = High",
    8);

  private static final String IDENTIFIER_STRING
  = "TigerVNC Configuration file Version 1.0";

  static VoidParameter[] parameterArray = {
    CSecurityTLS.X509CA,
    CSecurityTLS.X509CRL,
    SecurityClient.secTypes,
    alwaysCursor,
    cursorType,
    autoSelect,
    fullColor,
    lowColorLevel,
    preferredEncoding,
    customCompressLevel,
    compressLevel,
    noJpeg,
    qualityLevel,
    maximize,
    fullScreen,
    fullScreenAllMonitors,
    desktopSize,
    remoteResize,
    viewOnly,
    shared,
    acceptClipboard,
    sendClipboard,
    menuKey,
    noLionFS,
    sendLocalUsername,
    maxCutText,
    scalingFactor,
    acceptBell,
    via,
    tunnel,
    extSSH,
    extSSHClient,
    extSSHArgs,
    sshConfig,
    sshKeyFile,
  };

  static VoidParameter[] readOnlyParameterArray = {
    dotWhenNoCursor
  };


  static LogWriter vlog = new LogWriter("Parameters");

	public static void saveViewerParameters(String filename, String servername) {

	  // Write to the registry or a predefined file if no filename was specified.
    String filepath;
    if (filename == null || filename.isEmpty()) {
      saveToReg(servername);
      return;
    } else {
      filepath = filename;
    }

	  /* Write parameters to file */
    File f = new File(filepath);
    if (f.exists() && !f.canWrite())
	    throw new Exception(String.format("Failed to write configuration file,"+
                                        "can't open %s", filepath)); 

    PrintWriter pw = null;
    try {
      pw = new PrintWriter(f, "UTF-8");
    } catch (java.lang.Exception e)	{
      throw new Exception(e.getMessage());
    }

    pw.println(IDENTIFIER_STRING);
    pw.println("");

	  if (servername != null && !servername.isEmpty()) {
	    pw.println(String.format("ServerName=%s\n", servername));
      updateConnHistory(servername);
    }

    for (int i = 0; i < parameterArray.length; i++) {
      if (parameterArray[i] instanceof StringParameter) {
        //if (line.substring(0,idx).trim().equalsIgnoreCase(parameterArray[i].getName()))
          pw.println(String.format("%s=%s",parameterArray[i].getName(),
                                   parameterArray[i].getValueStr()));
      } else if (parameterArray[i] instanceof IntParameter) {
        //if (line.substring(0,idx).trim().equalsIgnoreCase(parameterArray[i].getName()))
          pw.println(String.format("%s=%s",parameterArray[i].getName(),
                                   parameterArray[i].getValueStr()));
      } else if (parameterArray[i] instanceof BoolParameter) {
        //if (line.substring(0,idx).trim().equalsIgnoreCase(parameterArray[i].getName()))
          pw.println(String.format("%s=%s",parameterArray[i].getName(),
                                   parameterArray[i].getValueStr()));
      } else {
        vlog.error(String.format("Unknown parameter type for parameter %s",
                   parameterArray[i].getName()));
      }
    }

    pw.flush();
    pw.close();
	}

  public static String loadViewerParameters(String filename) throws Exception {
    String servername = "";

    String filepath;
    if (filename == null) {
      return loadFromReg();
    } else {
      filepath = filename;
    }

    /* Read parameters from file */
    File f = new File(filepath);
    if (!f.exists() || !f.canRead()) {
      if (filename == null || filename.isEmpty())
        return null;
      throw new Exception(String.format("Failed to read configuration file, can't open %s",
                          filepath)); 
    }

    String line = "";
    LineNumberReader reader;
    try {
      reader = new LineNumberReader(new FileReader(f));
    } catch (FileNotFoundException e) {
      throw new Exception(e.getMessage());
    }

    int lineNr = 0;
    while (line != null) {

      // Read the next line
      try {
        line = reader.readLine();
        lineNr = reader.getLineNumber();
        if (line == null)
          break;
      } catch (IOException e) {
        throw new Exception(String.format("Failed to read line %d in file %s: %s",
          lineNr, filepath, e.getMessage()));
      }

      // Make sure that the first line of the file has the file identifier string
      if(lineNr == 1) {
        if(line.equals(IDENTIFIER_STRING))
          continue;
        else
          throw new Exception(String.format("Configuration file %s is in an invalid format", filename));
      }

      // Skip empty lines and comments
      if (line.trim().isEmpty() || line.trim().startsWith("#"))
        continue;

      // Find the parameter value
      int idx = line.indexOf("=");
      if (idx == -1) {
        vlog.error(String.format("Failed to read line %d in file %s: %s",
                   lineNr, filename, "Invalid format"));
        continue;
      }
      String value = line.substring(idx+1).trim();
      boolean invalidParameterName = true; // Will be set to false below if 
                                           // the line contains a valid name.

      if (line.substring(0,idx).trim().equalsIgnoreCase("ServerName")) {
        if (value.length() > 256) {
          vlog.error(String.format("Failed to read line %d in file %s: %s",
                     lineNr, filepath, "Invalid format or too large value"));
          continue;
        }
        servername = value;
        invalidParameterName = false;
      } else {
        for (int i = 0; i < parameterArray.length; i++) {
          VoidParameter parameter;
          if (i < parameterArray.length) {
            parameter = parameterArray[i];
          } else {
            parameter = readOnlyParameterArray[i - parameterArray.length];
          }
          if (parameter instanceof StringParameter) {
            if (line.substring(0,idx).trim().equalsIgnoreCase(parameter.getName())) {
              if (value.length() > 256) {
                vlog.error(String.format("Failed to read line %d in file %s: %s",
                           lineNr, filepath, "Invalid format or too large value"));
                continue;
              }
              ((StringParameter)parameter).setParam(value);
              invalidParameterName = false;
            }
          } else if (parameter instanceof IntParameter) {
            if (line.substring(0,idx).trim().equalsIgnoreCase(parameter.getName())) {
              ((IntParameter)parameter).setParam(value);
              invalidParameterName = false;
            }
          } else if (parameter instanceof BoolParameter) {
            if (line.substring(0,idx).trim().equalsIgnoreCase(parameter.getName())) {
              ((BoolParameter)parameter).setParam(value);
              invalidParameterName = false;
            }
          } else {
            vlog.error(String.format("Unknown parameter type for parameter %s",
                       parameter.getName()));

          }
        }
      }

      if (invalidParameterName)
        vlog.info(String.format("Unknown parameter %s on line %d in file %s",
                  line, lineNr, filepath));
    }
    try {
      reader.close();
    } catch (IOException e) {
      vlog.info(e.getMessage());
    } finally {
      try {
        if (reader != null)
          reader.close();
        } catch (IOException e) { }
    }

    return servername;
  }

  public static void saveToReg(String servername) {
    String hKey = "global";

    if (servername != null && !servername.isEmpty()) {
      UserPreferences.set(hKey, "ServerName", servername);
      updateConnHistory(servername);
    }

    for (int i = 0; i < parameterArray.length; i++) {
      if (parameterArray[i] instanceof StringParameter) {
        UserPreferences.set(hKey, parameterArray[i].getName(),
                                  parameterArray[i].getValueStr());
      } else if (parameterArray[i] instanceof IntParameter) {
        UserPreferences.set(hKey, parameterArray[i].getName(),
                            ((IntParameter)parameterArray[i]).getValue());
      } else if (parameterArray[i] instanceof BoolParameter) {
        UserPreferences.set(hKey, parameterArray[i].getName(),
                            ((BoolParameter)parameterArray[i]).getValue());
      } else {
        vlog.error(String.format("Unknown parameter type for parameter %s",
                   parameterArray[i].getName()));
      }
    }

    for (int i = 0; i < readOnlyParameterArray.length; i++) {
      UserPreferences.delete(hKey, readOnlyParameterArray[i].getName());
    }

    UserPreferences.save(hKey);
  }

  public static String loadFromReg() {

    String hKey = "global";

    String servername = UserPreferences.get(hKey, "ServerName");
    if (servername == null)
      servername = "";

    for (int i = 0; i < parameterArray.length + readOnlyParameterArray.length; i++) {
      VoidParameter parameter;
      if (i < parameterArray.length) {
        parameter = parameterArray[i];
      } else {
        parameter = readOnlyParameterArray[i - parameterArray.length];
      }
      if (parameter instanceof StringParameter) {
        if (UserPreferences.get(hKey, parameter.getName()) != null) {
          String stringValue =
            UserPreferences.get(hKey, parameter.getName());
          ((StringParameter)parameter).setParam(stringValue);
        }
      } else if (parameter instanceof IntParameter) {
        if (UserPreferences.get(hKey, parameter.getName()) != null) {
          int intValue =
            UserPreferences.getInt(hKey, parameter.getName());
          ((IntParameter)parameter).setParam(intValue);
        }
      } else if (parameter instanceof BoolParameter) {
        if (UserPreferences.get(hKey, parameter.getName()) != null) {
          boolean booleanValue =
            UserPreferences.getBool(hKey, parameter.getName());
          ((BoolParameter)parameter).setParam(booleanValue);
        }
      } else {
        vlog.error(String.format("Unknown parameter type for parameter %s",
                   parameter.getName()));
      }
    }

    return servername;
  }

  private static void updateConnHistory(String serverName) {
    String hKey = "ServerDialog";
    if (serverName != null && !serverName.isEmpty()) {
      String valueStr = UserPreferences.get(hKey, "history");
      String t = (valueStr == null) ? "" : valueStr;
      StringTokenizer st = new StringTokenizer(t, ",");
      StringBuffer sb = new StringBuffer().append(serverName);
      while (st.hasMoreTokens()) {
        String str = st.nextToken();
        if (!str.equals(serverName) && !str.equals(""))
          sb.append(',').append(str);
      }
      UserPreferences.set(hKey, "history", sb.toString());
      UserPreferences.save(hKey);
    }
  }

}
