/*
 * This file is part of Arduino.
 *
 * BasicUploader - generic command line uploader implementation
 *
 * Copyright (c) 2004-05
 * Hernando Barragan
 * Copyright (c) 2012
 * Cristian Maglie <c.maglie@arduino.cc>
 *
 * Arduino 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 program 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 program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * As a special exception, you may use this file as part of a free software
 * library without restriction.  Specifically, if other files instantiate
 * templates or use macros or inline functions from this file, or you compile
 * this file and link it with other files to produce an executable, this
 * file does not by itself cause the resulting executable to be covered by
 * the GNU General Public License.  This exception does not however
 * invalidate any other reasons why the executable file might be covered by
 * the GNU General Public License.
 */

package cc.arduino.packages.uploaders;

import cc.arduino.LoadVIDPIDSpecificPreferences;
import cc.arduino.packages.Uploader;
import processing.app.*;
import cc.arduino.packages.BoardPort;
import processing.app.debug.RunnerException;
import processing.app.debug.TargetPlatform;
import processing.app.helpers.PreferencesMap;
import processing.app.helpers.PreferencesMapException;
import processing.app.helpers.StringReplacer;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import static processing.app.I18n.tr;

public class SerialUploader extends Uploader {

  public SerialUploader()
  {
    super();
  }

  public SerialUploader(boolean noUploadPort)
  {
    super(noUploadPort);
  }

  @Override
  public boolean uploadUsingPreferences(File sourcePath, String buildPath, String className, boolean usingProgrammer, List<String> warningsAccumulator) throws Exception {
    // FIXME: Preferences should be reorganized
    TargetPlatform targetPlatform = BaseNoGui.getTargetPlatform();
    PreferencesMap prefs = PreferencesData.getMap();
    PreferencesMap boardPreferences = BaseNoGui.getBoardPreferences();
    if (boardPreferences != null) {
      prefs.putAll(boardPreferences);
    }
    String tool = prefs.getOrExcept("upload.tool");
    if (tool.contains(":")) {
      String[] split = tool.split(":", 2);
      targetPlatform = BaseNoGui.getCurrentTargetPlatformFromPackage(split[0]);
      tool = split[1];
    }
    prefs.putAll(targetPlatform.getTool(tool));

    if (programmerPid != null && programmerPid.isAlive()) {
      // kill the previous programmer
      programmerPid.destroyForcibly();
    }

    // if no protocol is specified for this board, assume it lacks a 
    // bootloader and upload using the selected programmer.
    if (usingProgrammer || prefs.get("upload.protocol") == null) {
      return uploadUsingProgrammer(buildPath, className);
    }

    BaseNoGui.getDiscoveryManager().getSerialDiscoverer().pausePolling(true);

    if (noUploadPort)
    {
      prefs.put("build.path", buildPath);
      prefs.put("build.project_name", className);
      if (verbose)
        prefs.put("upload.verbose", prefs.getOrExcept("upload.params.verbose"));
      else
        prefs.put("upload.verbose", prefs.getOrExcept("upload.params.quiet"));

      if (verifyUpload)
        prefs.put("upload.verify", prefs.get("upload.params.verify", ""));
      else
        prefs.put("upload.verify", prefs.get("upload.params.noverify", ""));

      try {
        return runCommand("upload.pattern", prefs);
      } finally {
        BaseNoGui.getDiscoveryManager().getSerialDiscoverer().pausePolling(false);
      }
    }

    // need to do a little dance for Leonardo and derivatives:
    // open then close the port at the magic baudrate (usually 1200 bps) first
    // to signal to the sketch that it should reset into bootloader. after doing
    // this wait a moment for the bootloader to enumerate. On Windows, also must
    // deal with the fact that the COM port number changes from bootloader to
    // sketch.
    boolean doTouch = prefs.getBoolean("upload.use_1200bps_touch");
    boolean waitForUploadPort = prefs.getBoolean("upload.wait_for_upload_port");

    String userSelectedUploadPort = prefs.get("serial.port", "");
    String actualUploadPort = null;

    if (doTouch) {
      try {
        // Toggle 1200 bps on selected serial port to force board reset.
        List<String> before = Serial.list();
        if (before.contains(userSelectedUploadPort)) {
          if (verbose)
            System.out.println(
              I18n.format(tr("Forcing reset using 1200bps open/close on port {0}"), userSelectedUploadPort));
          Serial.touchForCDCReset(userSelectedUploadPort);
        }
        Thread.sleep(400);
        if (waitForUploadPort) {
          // Scanning for available ports seems to open the port or
          // otherwise assert DTR, which would cancel the WDT reset if
          // it happened within 250 ms. So we wait until the reset should
          // have already occurred before we start scanning.
          actualUploadPort = waitForUploadPort(userSelectedUploadPort, before);

    	  // on OS X, if the port is opened too quickly after it is detected,
    	  // a "Resource busy" error occurs, add a delay to workaround this,
          // apply to other platforms as well.
    	  Thread.sleep(250);
        }
      } catch (SerialException e) {
        throw new RunnerException(e);
      } catch (InterruptedException e) {
        throw new RunnerException(e.getMessage());
      } finally {
        BaseNoGui.getDiscoveryManager().getSerialDiscoverer().pausePolling(false);
      }
      if (actualUploadPort == null) {
        actualUploadPort = userSelectedUploadPort;
      }
      prefs.put("serial.port", actualUploadPort);
      if (actualUploadPort.startsWith("/dev/")) {
        prefs.put("serial.port.file", actualUploadPort.substring(5));
      } else {
        prefs.put("serial.port.file", actualUploadPort);
      }

      // retrigger a discovery
      BaseNoGui.getDiscoveryManager().getSerialDiscoverer().setUploadInProgress(true);
      Thread.sleep(100);
      BaseNoGui.getDiscoveryManager().getSerialDiscoverer().forceRefresh();
      Thread.sleep(100);
    }

    BoardPort boardPort = BaseNoGui.getDiscoveryManager().find(PreferencesData.get("serial.port", ""));
    try {
      prefs.put("serial.port.iserial", boardPort.getPrefs().getOrExcept("iserial"));
    } catch (Exception e) {
      // if serial port does not contain an iserial field
    }

    prefs.put("build.path", buildPath);
    prefs.put("build.project_name", className);
    if (verbose) {
      prefs.put("upload.verbose", prefs.getOrExcept("upload.params.verbose"));
    } else {
      prefs.put("upload.verbose", prefs.getOrExcept("upload.params.quiet"));
    }

    if (verifyUpload)
      prefs.put("upload.verify", prefs.get("upload.params.verify", ""));
    else
      prefs.put("upload.verify", prefs.get("upload.params.noverify", ""));

    boolean uploadResult;
    try {
      uploadResult = runCommand("upload.pattern", prefs);
    } finally {
      BaseNoGui.getDiscoveryManager().getSerialDiscoverer().pausePolling(false);
    }

    String finalUploadPort = null;
    if (uploadResult && doTouch) {
      try {
        if (waitForUploadPort) {
          // For Due/Leonardo wait until the bootloader serial port disconnects and the
          // sketch serial port reconnects (or timeout after a few seconds if the
          // sketch port never comes back). Doing this saves users from accidentally
          // opening Serial Monitor on the soon-to-be-orphaned bootloader port.

          // Reuse waitForUploadPort for this task, but this time we are simply waiting
          // for one port to reappear. If no port reappears before the timeout, actualUploadPort is selected
          finalUploadPort = waitForUploadPort(actualUploadPort, Serial.list(), false, 2000);
        }
      } catch (RunnerException ex) {
        // noop
      }
    }

    if (finalUploadPort == null) {
      finalUploadPort = userSelectedUploadPort;
    }
    BaseNoGui.selectSerialPort(finalUploadPort);

    BaseNoGui.getDiscoveryManager().getSerialDiscoverer().setUploadInProgress(false);
    BaseNoGui.getDiscoveryManager().getSerialDiscoverer().pausePolling(false);

    return uploadResult;
  }

  private String waitForUploadPort(String uploadPort, List<String> before) throws InterruptedException, RunnerException {
	  return waitForUploadPort(uploadPort, before, verbose, 10000);
  }

  private String waitForUploadPort(String uploadPort, List<String> before, boolean verbose, int timeout) throws InterruptedException, RunnerException {
    // Wait for a port to appear on the list
    int elapsed = 0;
    while (elapsed < timeout) {
      List<String> now = Serial.list();
      List<String> diff = new ArrayList<>(now);
      diff.removeAll(before);
      if (verbose) {
        System.out.print("PORTS {");
        for (String p : before)
          System.out.print(p + ", ");
        System.out.print("} / {");
        for (String p : now)
          System.out.print(p + ", ");
        System.out.print("} => {");
        for (String p : diff)
          System.out.print(p + ", ");
        System.out.println("}");
      }
      if (diff.size() > 0) {
        String newPort = diff.get(0);
        if (verbose)
          System.out.println("Found upload port: " + newPort);
        return newPort;
      }

      // Keep track of port that disappears
      before = now;
      Thread.sleep(250);
      elapsed += 250;

      // On Windows and OS X, it can take a few seconds for the port to disappear and
      // come back, so use a time out before assuming that the selected port is the
      // bootloader (not the sketch).
      if (elapsed >= 5000 && now.contains(uploadPort)) {
        if (verbose)
          System.out.println("Uploading using selected port: " + uploadPort);
        return uploadPort;
      }
    }

    // Something happened while detecting port
    throw new RunnerException(tr("Couldn't find a Board on the selected port. Check that you have the correct port selected.  If it is correct, try pressing the board's reset button after initiating the upload."), false);
  }

  private boolean uploadUsingProgrammer(String buildPath, String className) throws Exception {

    TargetPlatform targetPlatform = BaseNoGui.getTargetPlatform();
    String programmer = PreferencesData.get("programmer");
    if (programmer.contains(":")) {
      String[] split = programmer.split(":", 2);
      targetPlatform = BaseNoGui.getCurrentTargetPlatformFromPackage(split[0]);
      programmer = split[1];
    }

    PreferencesMap prefs = PreferencesData.getMap();
    PreferencesMap boardPreferences = BaseNoGui.getBoardPreferences();
    if (boardPreferences != null) {
      prefs.putAll(boardPreferences);
    }
    PreferencesMap programmerPrefs = targetPlatform.getProgrammer(programmer);
    if (programmerPrefs == null)
      throw new RunnerException(
          tr("Please select a programmer from Tools->Programmer menu"));
    prefs.putAll(targetPlatform.getTool(programmerPrefs.getOrExcept("program.tool")));
    prefs.putAll(programmerPrefs);

    prefs.put("build.path", buildPath);
    prefs.put("build.project_name", className);

    if (verbose)
      prefs.put("program.verbose", prefs.getOrExcept("program.params.verbose"));
    else
      prefs.put("program.verbose", prefs.getOrExcept("program.params.quiet"));

    if (verifyUpload)
      prefs.put("program.verify", prefs.get("program.params.verify", ""));
    else
      prefs.put("program.verify", prefs.get("program.params.noverify", ""));

    return runCommand("program.pattern", prefs);
  }

  @Override
  public boolean burnBootloader() throws Exception {
    TargetPlatform targetPlatform = BaseNoGui.getTargetPlatform();

    // Find preferences for the selected programmer
    PreferencesMap programmerPrefs;
    String programmer = PreferencesData.get("programmer");
    if (programmer.contains(":")) {
      String[] split = programmer.split(":", 2);
      TargetPlatform platform = BaseNoGui.getCurrentTargetPlatformFromPackage(split[0]);
      programmer = split[1];
      programmerPrefs = platform.getProgrammer(programmer);
    } else {
      programmerPrefs = targetPlatform.getProgrammer(programmer);
    }
    if (programmerPrefs == null)
      throw new RunnerException(
          tr("Please select a programmer from Tools->Programmer menu"));

    // Build configuration for the current programmer
    PreferencesMap prefs = PreferencesData.getMap();
    PreferencesMap boardPreferences = BaseNoGui.getBoardPreferences();
    if (boardPreferences != null) {
      prefs.putAll(boardPreferences);
    }
    prefs.putAll(programmerPrefs);

    // Create configuration for bootloader tool
    PreferencesMap toolPrefs = new PreferencesMap();
    String tool = prefs.getOrExcept("bootloader.tool");
    if (tool.contains(":")) {
      String[] split = tool.split(":", 2);
      TargetPlatform platform = BaseNoGui.getCurrentTargetPlatformFromPackage(split[0]);
      tool = split[1];
      toolPrefs.putAll(platform.getTool(tool));
      if (toolPrefs.size() == 0)
        throw new RunnerException(I18n.format(tr("Could not find tool {0} from package {1}"), tool, split[0]));
    }
    toolPrefs.putAll(targetPlatform.getTool(tool));
    if (toolPrefs.size() == 0)
      throw new RunnerException(I18n.format(tr("Could not find tool {0}"), tool));

    // Merge tool with global configuration
    prefs.putAll(toolPrefs);
    if (verbose) {
      prefs.put("erase.verbose", prefs.getOrExcept("erase.params.verbose"));
      prefs.put("bootloader.verbose", prefs.getOrExcept("bootloader.params.verbose"));
    } else {
      prefs.put("erase.verbose", prefs.getOrExcept("erase.params.quiet"));
      prefs.put("bootloader.verbose", prefs.getOrExcept("bootloader.params.quiet"));
    }

    new LoadVIDPIDSpecificPreferences().load(prefs);

    if (!runCommand("erase.pattern", prefs))
      return false;

    return runCommand("bootloader.pattern", prefs);
  }

  private boolean runCommand(String patternKey, PreferencesMap prefs) throws Exception, RunnerException {
    try {
      String pattern = prefs.getOrExcept(patternKey);
      StringReplacer.checkIfRequiredKeyIsMissingOrExcept("serial.port", pattern, prefs);
      String[] cmd = StringReplacer.formatAndSplit(pattern, prefs);
      return executeUploadCommand(cmd);
    } catch (RunnerException e) {
      throw e;
    } catch (PreferencesMapException e) {
      if (e.getMessage().equals("serial.port")) {
        throw new SerialNotFoundException(e);
      }
      throw e;
    } catch (Exception e) {
      throw new RunnerException(e);
    }
  }
}
