/*
Copyright 2011-2016 Google Inc. All Rights Reserved.

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.
*/
package com.google.security.zynamics.zylib.gui.license;

import com.google.security.zynamics.zylib.gui.CMessageBox;
import com.google.security.zynamics.zylib.gui.ProgressDialogs.CEndlessHelperThread;
import com.google.security.zynamics.zylib.gui.ProgressDialogs.CEndlessProgressDialog;
import com.google.security.zynamics.zylib.io.StreamUtils;

import java.awt.Window;
import java.io.FileNotFoundException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;


/**
 * Helper class that provides a common starting point for update checks in zynamics products.
 * 
 * @author cblichmann@google.com (Christian Blichmann)
 */
public class UpdateCheckHelper {
  /**
   * Base URL prefix for the update check mechanism. Make sure it does not end with a slash.
   */
  public static String UPDATE_CHECK_BASE_URL = "http://www.zynamics.com/updates";

  private UpdateCheckHelper() {
    // Static methods only
  }

  /**
   * Checks for available product updates in the "stable" channel and displays the results in a user
   * interface.
   * 
   * @param parent a {@link Window} to be used as a parent for modal dialogs. Can be {@code null}.
   * @param productName the name of the product to check for updates. Should not include a leading
   *        "zynamics ".
   * @param currentVersion the version string of the product that denotes the current version (i.e.
   *        "4.0.0").
   */
  public static void checkForUpdatesWithUi(final Window parent, final String productName,
      final String currentVersion) {
    checkForUpdatesWithUi(parent, productName, currentVersion, "stable");
  }

  /**
   * Checks for available product updates and displays the results in a user interface.
   * 
   * @param parent a {@link Window} to be used as a parent for modal dialogs. Can be {@code null}.
   * @param productName the name of the product to check for updates. Should not include a leading
   *        "zynamics ".
   * @param currentVersion the version string of the product that denotes the current version (i.e.
   *        "4.0.0").
   * @param currentChannel the release channel to check for updates. This can be used to distinguish
   *        beta versions ("testing") from release versions ("stable").
   */
  public static void checkForUpdatesWithUi(final Window parent, final String productName,
      final String currentVersion, final String currentChannel) {
    final URL checkUrl;
    try {
      checkUrl =
          new URL(String.format("%s/%s/%s", UPDATE_CHECK_BASE_URL, productName.toLowerCase(),
              currentChannel.toLowerCase()));
    } catch (final MalformedURLException e) {
      // Should never happen
      throw new RuntimeException("Malformed URL template", e);
    }

    final List<String> lines = new ArrayList<String>();
    final CEndlessHelperThread helper = new CEndlessHelperThread() {
      @Override
      protected void runExpensiveCommand() throws Exception {
        final long startTime = new Date().getTime();

        // Read the actual version info
        lines.addAll(StreamUtils.readLinesFromReader(new InputStreamReader(checkUrl.openStream())));

        // Make this dialog visible for at least 400 msec so that users
        // have a change to read the "Checking for updates..." text.
        final long duration = new Date().getTime() - startTime;
        if (duration < 400) {
          Thread.sleep(400 - duration);
        }
      }
    };
    CEndlessProgressDialog.show(parent,
        System.getProperty(CMessageBox.DEFAULT_WINDOW_TITLE_PROPERTY), "Checking for updates...",
        helper);
    final Exception e = helper.getException();
    if (e instanceof FileNotFoundException) {
      CMessageBox.showWarning(parent, "Could not check for updates. "
          + "The update site is unavailable.");
      return;
    }

    if (lines.isEmpty()) {
      CMessageBox.showWarning(parent, "Could not check for updates. "
          + "The update site returned no data.");
      return;
    }

    // Make sure we have at least 2 lines and the second one is a date of
    // the form YYYY-MM-DD (to be used later)
    if ((lines.size() < 2) || !lines.get(1).matches("\\d{4}-\\d\\d-\\d\\d")) {
      CMessageBox.showWarning(parent, "Could not check for updates. "
          + "Could not parse the response.");
      return;
    }

    final String remoteVersion = lines.get(0);
    final int result = versionCompare(currentVersion, remoteVersion);
    if (result < 0) {
      CMessageBox.showInformation(parent,
          String.format("A newer version (%s) is available.", remoteVersion));
      return;
    }
    if (result >= 0) {
      CMessageBox.showInformation(parent,
          String.format("Your version of zynamics %s is up to date.", productName));
      return;
    }
  }

  /**
   * Compares two version strings component-wise. Use to compare componentized version strings like
   * "3.2.2 beta 1" and "10.1". Both strings are split using the regular expression "\\.|-|\\s+".
   * 
   * @param current the source version string
   * @param remote the target version string to compare against
   * @return -1, 0 or 1 as the specified current version string is less than, equal to or greater
   *         than the specified target version string
   */
  public static int versionCompare(final String current, final String remote) {
    final String[] curComp = current.trim().split("\\.|-|\\s+");
    final String[] remComp = remote.trim().split("\\.|-|\\s+");
    int result = 0;
    for (int i = 0; (result == 0) && (i < Math.min(curComp.length, remComp.length)); i++) {
      try {
        // Try numerical comparison first
        result = Integer.parseInt(curComp[i]) - Integer.parseInt(remComp[i]);
      } catch (final NumberFormatException e) {
        // Fall back to lexical comparison (for version components
        // like "beta")
        result = curComp[i].compareToIgnoreCase(remComp[i]);
      }
    }

    // Clamp result to -1, 0 and 1 respectively
    return result < -1 ? -1 : (result > 1 ? 1 : result);
  }
}
