/*  ShuffleMove - A program for identifying and simulating ideal moves in the game
 *  called Pokemon Shuffle.
 *  
 *  Copyright (C) 2015  Andrew Meyers
 *  
 *  This program 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 3 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, see <http://www.gnu.org/licenses/>.
 */

package shuffle.fwk.update;

import java.awt.Desktop;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import shuffle.fwk.config.EntryType;
import shuffle.fwk.i18n.I18nUser;
import shuffle.fwk.service.update.UpdateService;

public class UpdateCheck implements I18nUser {
   private static final Logger LOG = Logger.getLogger(UpdateCheck.class.getName());
   private static final String VERSION_SITE = "http://loresoftworks.noip.me/shuffleversion.html";
   private static final String VERSION_REGEX = "(v\\d+\\.\\d+\\.\\d+).*href=\"(http\\S+)\">";
   private static final Pattern VERSION_PATTERN = Pattern.compile(VERSION_REGEX);
   private static final String VERSION_EXTRACT = "v(\\d+)\\.(\\d+)\\.(\\d+)";
   private static final Pattern VERSION_EXTRACT_PATTERN = Pattern.compile(VERSION_EXTRACT);
   private static final String ZIP_NAME = "Shuffle Move %s.zip";
   
   private static final String KEY_CORRUPT_PATH = "updatecheck.site.corruptpath";
   private static final String KEY_SITE_IOEXCEPTION = "updatecheck.site.ioexception";
   private static final String KEY_SITE_USEMANUAL = "updatecheck.site.pleaseusemanual";
   private static final String KEY_DOWNLOAD_READY = "updatecheck.ready";
   private static final String KEY_NOUPDATES = "updatecheck.noupdates";
   private static final String KEY_UPTODATE = "updatecheck.uptodate";
   private static final String KEY_OUTOFDATE = "updatecheck.outofdate";
   private static final String KEY_GET_IOEXCEPTION = "updatecheck.get.ioexception";
   private static final String KEY_GET_USEMANUAL = "updatecheck.get.pleaseusemanual";
   private static final String KEY_GET_INVALID = "updatecheck.get.versioninvalid";
   private static final String KEY_GET_IOEXCEPTION_OPEN = "updatecheck.get.ioexception.open";
   
   public static final ExecutorService EXECUTOR = new ScheduledThreadPoolExecutor(1);
   public static final String PROPERTY_DONE = "PROPERTY_DONE";
   public static final String PROPERTY_MESSAGE = "MESSAGE";
   public static final String NO_VERSION = "v0.0.0";
   
   public Map<String, String> getAvailableVersions() {
      Map<String, String> ret = new HashMap<String, String>();
      try (InputStream is = new URL(VERSION_SITE).openStream()) {
         BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
         String line;
         while ((line = br.readLine()) != null) {
            Matcher m = VERSION_PATTERN.matcher(line);
            if (m.find()) {
               ret.put(m.group(1), m.group(2));
            }
         }
      } catch (MalformedURLException mue) {
         LOG.log(Level.SEVERE, getString(KEY_CORRUPT_PATH), mue);
      } catch (IOException ioe) {
         LOG.log(Level.SEVERE, getString(KEY_SITE_IOEXCEPTION), ioe);
         LOG.warning(getString(KEY_SITE_USEMANUAL));
      }
      return ret;
   }
   
   public static void main(String[] args) {
      String path = "http://www.serebii.net/shuffle/pokemon/%s.png";
      String savePath = "downloads/%s.png";
      List<String> namesToTry = Arrays.asList("%s", "%s-m", "%s-mx", "%s-my");
      NumberFormat format = new DecimalFormat("000");
      TreeSet<Integer> megas = new TreeSet<Integer>(Arrays.asList(15, 18, 80, 208, 254, 260, 302, 323, 334, 362, 373,
            376, 380, 381, 384, 428, 475, 531, 719, 3, 6, 9, 65, 94, 115, 127, 130, 142, 150, 181, 212, 214, 229, 248,
            257, 282, 303, 306, 308, 310, 354, 359, 445, 448, 460));
      for (int i = 1; i < 723; i++) {
         String name = format.format(i);
         if (megas.contains(i)) {
            for (String nameToTry : namesToTry) {
               String fileName = String.format(nameToTry, name);
               String saveAt = String.format(savePath, fileName);
               String loadFrom = String.format(path, fileName);
               saveToFile(saveAt, loadFrom);
            }
         } else {
            String fileName = String.format(namesToTry.get(0), name);
            String saveAt = String.format(savePath, fileName);
            String loadFrom = String.format(path, fileName);
            saveToFile(saveAt, loadFrom);
         }
      }
   }
   
   public boolean isNewestVersion(String curVersion, Map<String, String> availableVersions) {
      String newestVersion = getNewestVersion(availableVersions);
      return getVersionNumber(curVersion) >= getVersionNumber(newestVersion);
   }
   
   /**
    * @return
    */
   public String getNewestVersion(Map<String, String> availableVersions) {
      String newestVersion = NO_VERSION;
      if (!availableVersions.isEmpty()) {
         for (String v : availableVersions.keySet()) {
            if (getVersionNumber(v) > getVersionNumber(newestVersion)) {
               newestVersion = v;
            }
         }
      }
      return newestVersion;
   }
   
   public void doUpdate(String curVersion, boolean force) {
      doUpdate(curVersion, force, null);
   }
   
   public void doUpdate(String curVersion, boolean force, PropertyChangeListener listener) {
      Map<String, String> availableVersions = getAvailableVersions();
      boolean listenerHandled = false;
      if (!availableVersions.isEmpty()) {
         String newestVersion = getNewestVersion(availableVersions);
         String target = versionCheck(curVersion, availableVersions);
         if (force) {
            target = availableVersions.get(newestVersion);
         }
         if (target == null) {
            listener.propertyChange(new PropertyChangeEvent(this, PROPERTY_MESSAGE, "", UpdateService.KEY_UPTODATE));
         } else {
            File file = null;
            try {
               file = getFile(getZipName(newestVersion));
            } catch (IOException e) {
               String ioExceptionString = getString(KEY_GET_IOEXCEPTION, newestVersion);
               LOG.log(Level.SEVERE, ioExceptionString, e);
               LOG.warning(getString(KEY_GET_USEMANUAL));
            }
            if (file != null && file.exists() && !force) {
               LOG.warning(getString(KEY_DOWNLOAD_READY) + " " + file.getAbsolutePath());
               listener.propertyChange(new PropertyChangeEvent(this, PROPERTY_MESSAGE, "",
                     UpdateService.KEY_PLEASE_UNPACK));
               listenerHandled = true;
               showParentOf(file);
            } else {
               getVersion(newestVersion, target, listener);
               listenerHandled = true;
            }
         }
      } else {
         LOG.warning(getString(KEY_NOUPDATES));
      }
      if (!listenerHandled && listener != null) {
         listener.propertyChange(new PropertyChangeEvent(this, PROPERTY_DONE, 0, 0));
      }
   }

   /**
    * @param file
    *           The file to show the folder of.
    */
   public void showParentOf(File file) {
      Desktop d = Desktop.getDesktop();
      try {
         d.open(file.getParentFile());
      } catch (IOException e) {
         String message = getString(KEY_GET_IOEXCEPTION_OPEN, file.getAbsolutePath());
         LOG.log(Level.SEVERE, message, e);
      }
   }
   
   public static void showParentDirectoryOf(File file) {
      new UpdateCheck().showParentOf(file);
   }

   public String versionCheck(String curVersion, Map<String, String> availableVersions) {
      String newestVersion = getNewestVersion(availableVersions);
      String target = null;
      if (getVersionNumber(curVersion) >= getVersionNumber(newestVersion)) {
         LOG.info(getString(KEY_UPTODATE, newestVersion, curVersion));
      } else {
         LOG.info(getString(KEY_OUTOFDATE));
         target = availableVersions.get(newestVersion);
      }
      return target;
   }
   
   /**
    * @param newestVersion
    */
   public void getVersion(String newestVersion, String fileUrl, PropertyChangeListener listener) {
      EXECUTOR.submit(new Runnable() {
         @Override
         public void run() {
            try (InputStream stream = new URL(fileUrl).openStream()) {
               File file = getFile(getZipName(newestVersion));
               Files.copy(stream, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
               LOG.warning(getString(KEY_DOWNLOAD_READY) + " " + file.getAbsolutePath());
               listener.propertyChange(new PropertyChangeEvent(this, PROPERTY_MESSAGE, "",
                     UpdateService.KEY_PLEASE_UNPACK));
               showParentOf(file);
            } catch (IOException e) {
               String ioExceptionString = getString(KEY_GET_IOEXCEPTION, newestVersion);
               LOG.log(Level.SEVERE, ioExceptionString, e);
               LOG.warning(getString(KEY_GET_USEMANUAL));
               listener.propertyChange(new PropertyChangeEvent(this, PROPERTY_MESSAGE, "", ioExceptionString));
            } finally {
               listener.propertyChange(new PropertyChangeEvent(this, PROPERTY_DONE, 0, 0));
            }
         }
      });
   }
   
   private File getFile(String path) throws IOException {
      try {
         return (File) EntryType.FILE.parseValue(null, path);
      } catch (Exception e) {
         if (e instanceof IOException) {
            throw (IOException) e;
         }
      }
      return null;
   }
   
   private static void saveToFile(String fileName, String fileUrl) {
      try (InputStream stream = new URL(fileUrl).openStream()) {
         // System.out.println("Saving to path: " + path.getFileName());
         Files.copy(stream, Paths.get(fileName), StandardCopyOption.REPLACE_EXISTING);
      } catch (IOException e) {
         System.out.println("Bad path: " + e.getMessage());
      }
   }
   
   /**
    * @param newestVersion
    * @return
    */
   public static String getZipName(String newestVersion) {
      return String.format(ZIP_NAME, newestVersion);
   }
   
   public int getVersionNumber(String version) {
      int ret = 0;
      try {
         ret = parseVersionNumber(version);
      } catch (NumberFormatException e) {
         LOG.log(Level.SEVERE, getString(KEY_GET_INVALID, version), e);
      }
      return ret;
   }
   
   public int[] getVersionNumbers(String version) {
      int[] ret = new int[3];
      try {
         Matcher m = VERSION_EXTRACT_PATTERN.matcher(version);
         if (m.find()) {
            for (int i = 0; i < 3; i++) {
               ret[i] = Math.max(0, Integer.parseInt(m.group(i + 1)));
            }
         }
      } catch (NumberFormatException e) {
         LOG.log(Level.SEVERE, getString(KEY_GET_INVALID, version), e);
      }
      return ret;
   }

   public static String getVersionString(int version) {
      String versionString;
      if (version <= 0) {
         versionString = "v0.0.0";
      } else {
         int subminor = version % 1000;
         int remainder = version / 1000;
         int minor = remainder % 1000;
         int major = remainder / 1000;
         versionString = String.format("v%d.%d.%d", major, minor, subminor);
      }
      return versionString;
   }
   
   public static int parseVersionNumber(String version) throws NumberFormatException {
      Matcher m = VERSION_EXTRACT_PATTERN.matcher(version);
      int ret = 0;
      if (m.find()) {
         for (int i = 0; i < 3; i++) {
            ret += Math.pow(1000, i) * Math.max(0, Integer.parseInt(m.group(3 - i)));
         }
      }
      return ret;
   }
}