"use strict";

var logger = require('./server/logger.js').get('appium')
  , fs = require('fs')
  , ncp = require('ncp').ncp
  , request = require('request')
  , _ = require('underscore')
  , path = require('path')
  , rimraf = require('rimraf')
  , exec = require('child_process').exec
  , osType = require('os').type()
  , tempdir = require('./tempdir')
  , glob = require('glob')
  , AdmZip = require('adm-zip');


exports.downloadFile = function (fileUrl, suffix, cb) {
  // We will be downloading the files to a directory, so make sure it's there
  // This step is not required if you have manually created the directory
  tempdir.open({prefix: 'appium-app', suffix: suffix}, function (err, info) {
    var file = fs.createWriteStream(info.path);
    request(fileUrl).pipe(file).on('close', function () {
      logger.info(fileUrl + ' downloaded to ' + info.path);
      cb(info.path);
    });
  });
};

exports.copyLocalZip = function (localZipPath, cb) {
  logger.info("Copying local zip to tmp dir");
  fs.stat(localZipPath, function (err) {
    if (err) return cb(err);
    tempdir.open({prefix: 'appium-app', suffix: '.zip'}, function (err, info) {
      var infile = fs.createReadStream(localZipPath);
      var outfile = fs.createWriteStream(info.path);
      infile.pipe(outfile).on('close', function () {
        logger.info(localZipPath + ' copied to ' + info.path);
        cb(null, info.path);
      });
    });
  });
};

exports.unzipFile = function (zipPath, cb) {
  logger.info("Unzipping " + zipPath);
  exports.testZipArchive(zipPath, function (err, valid) {
    if (valid) {
      if (exports.isWindows()) {
        var zip = new AdmZip(zipPath);
        zip.extractAllTo(path.dirname(zipPath), true);
        logger.info("Unzip successful");
        cb(null, null);
      } else {
        var execOpts = {cwd: path.dirname(zipPath), maxBuffer: 524288};
        exec('unzip -o ' + zipPath, execOpts, function (err, stderr, stdout) {
          if (!err) {
            logger.info("Unzip successful");
            cb(null, stderr);
          } else {
            logger.error("Unzip threw error " + err);
            logger.error("Stderr: " + stderr);
            logger.error("Stdout: " + stdout);
            cb("Archive could not be unzipped, check appium logs.", null);
          }
        });
      }
    } else {
      cb(err, null);
    }
  });
};

exports.testZipArchive = function (zipPath, cb) {
  logger.info("Testing zip archive: " + zipPath);
  if (exports.isWindows()) {
    if (fs.existsSync(zipPath)) {
      logger.info("Zip archive tested clean");
      cb(null, true);
    } else {
      cb("Zip archive was not found.", false);
    }
  } else {
    var execOpts = {cwd: path.dirname(zipPath)};
    exec("unzip -tq " + zipPath, execOpts, function (err, stderr, stdout) {
      if (!err) {
        if (/No errors detected/.exec(stderr)) {
          logger.info("Zip archive tested clean");
          cb(null, true);
        } else {
          logger.error("Zip file " + zipPath + " was not valid");
          logger.error("Stderr: " + stderr);
          logger.error("Stdout: " + stdout);
          cb("Zip archive did not test successfully, check appium server logs " +
             "for output", false);
        }
      } else {
        logger.error("Test zip archive threw error " + err);
        logger.error("Stderr: " + stderr);
        logger.error("Stdout: " + stdout);
        cb("Error testing zip archive, are you sure this is a zip file? " + err, null);
      }
    });
  }
};

exports.unzipApp = function (zipPath, appExt, cb) {
  exec("find " + path.dirname(zipPath) + " -type d -name '*" + appExt + "' | xargs rm -rf " + path.dirname(zipPath) +
    "/Payload*", function (error /*, stdout, stderr*/) {
    if (!error) {
      exports.unzipFile(zipPath, function (err, output) {
        if (!err) {
          var relaxedRegStr = "(?:creating|inflating|extracting): (.+" + appExt + ")/?";
          // in the strict regex, we check for an entry which ends with the
          // extension
          var strictReg = new RegExp(relaxedRegStr + "$", 'm');
          // otherwise, we allow an entry which contains the extension, but we
          // need to be careful, because it might be a false positive
          var relaxedReg = new RegExp(relaxedRegStr, 'm');
          var strictMatch = strictReg.exec(output);
          var relaxedMatch = relaxedReg.exec(output);
          var getAppPath = function (match) {
            return path.resolve(path.dirname(zipPath), match[1]);
          };
          if (strictMatch) {
            cb(null, getAppPath(strictMatch));
          } else if (relaxedMatch) {
            logger.info("Got a relaxed match for app in zip, be careful for app match errors");
            cb(null, getAppPath(relaxedMatch));
          } else {
            cb("App zip unzipped OK, but we couldn't find a .app bundle in it. " +
              "Make sure your archive contains the .app package and nothing else",
              null);
          }
        } else {
          cb(err, null);
        }
      });
    } else {
      cb(error, null);
    }
  });
};

exports.checkBuiltInApp = function (appName, version, cb) {
  logger.info("Looking for built in app " + appName);
  var newAppDir = path.resolve(exports.getAppsBackupPath(),
      'Appium-' + appName + '.app');
  var checkApp = function (s, appPath, cb) {
    if (!s.isDirectory()) {
      cb(new Error("App package was not a directory"), appPath);
      return false;
    }
    return true;
  };
  exports.getBuiltInAppDir(version, function (err, appDir) {
    if (err) return cb(err);
    var appPath = path.resolve(appDir, appName + ".app");
    fs.stat(appPath, function (err, s) {
      if (err && err.message.indexOf("ENOENT") !== -1) {
        logger.info("App is not at " + appPath);
        fs.stat(newAppDir, function (err, s) {
          if (err) {
            logger.warn("App is also not at " + newAppDir);
            return cb(new Error("Couldn't find built in app in its home " +
                                "or temp dir!"));
          }
          if (checkApp(s, appPath, cb)) {
            logger.info("Couldn't find original app, but found the temp " +
                        "Appium one so using that");
            cb(null, newAppDir, appPath);
          }
        });
        return;
      } else if (err) {
        return cb(err);
      }
      if (checkApp(s, appPath, cb)) {
        if (parseInt(version, 10) < 7) {
          logger.info("Got app, trying to copy " + appPath + " to tmp dir");
          ncp(appPath, newAppDir, function (err) {
            if (err) return cb(err);
            logger.info("Copied " + appName);
            cb(null, newAppDir, null);
          });
        } else {
          logger.info("Got app, trying to move " + appPath + " to tmp dir");
          exports.moveBuiltInApp(appPath, appName, newAppDir, cb);
        }
      }
    });
  });
};

exports.checkSafari = function (version, cb) {
  exports.checkBuiltInApp("MobileSafari", version, cb);
};

exports.checkPreferencesApp = function (version, cb) {
  exports.checkBuiltInApp("Preferences", version, cb);
};

exports.getBuiltInAppDir = function (version, cb) {
  exports.getXcodeFolder(function (err, xcodeDir) {
    if (err) return cb(err);

    var appDir = path.resolve(xcodeDir, "Platforms/iPhoneSimulator.platform/" +
                              "Developer/SDKs/iPhoneSimulator" + version +
                              ".sdk/Applications/");
    fs.stat(appDir, function (err, s) {
      if (err) {
        cb(err);
      } else if (!s.isDirectory()) {
        cb(new Error("Could not load built in applications directory"));
      } else {
        cb(null, appDir);
      }
    });
  });
};

exports.moveBuiltInApp = function (appPath, appName, newAppDir, cb) {
  ncp(appPath, newAppDir, function (err) {
    if (err) return cb(err);
    logger.info("Copied " + appName + " to " + newAppDir);
    rimraf(appPath, function (err) {
      if (err) {
        if (err.message.indexOf("EACCES") !== -1) {
          return cb(new Error("We don't have write access to " + appPath +
                    ", please re-run authorize as " + process.env.USER));
        }
        return cb(err);
      }
      logger.info("Temporarily deleted original app at " + appPath);
      cb(null, newAppDir, appPath);
    });
  });
};

exports.cleanSafari = function (safariVer, cb) {
  var baseDir = "Library/Application Support/iPhone Simulator/" +
                       safariVer + "*/Library/";
  exports.getUser(function (err, user) {
    if (err) {
      logger.error(err);
      cb(err);
    } else {
      var baseSupportDirs = path.resolve("/Users", user, baseDir);
      glob(baseSupportDirs, function (err, pathList) {
        if (err) return cb(err);
        if (pathList.length === 0) {
          logger.info("Couldn't find Safari support directories in order " +
                      "to clear out old data");
          return cb();
        }
        var pathsFinished = 0;
        var errToRet = null;
        var finishAll = function (err) {
          if (err) errToRet = err;
          pathsFinished++;
          if (pathsFinished === pathList.length) {
            cb(err);
          }
        };
        _.each(pathList, function (baseSupportDir) {
          var toDeletes = [
            'Caches/Snapshots/com.apple.mobilesafari'
            , 'Caches/com.apple.mobilesafari/Cache.db*'
            , 'Caches/com.apple.WebAppCache/*.db'
            , 'Safari/*.plist'
            , 'WebKit/LocalStorage/*.*'
            , 'Library/WebKit/GeolocationSites.plist'
            , 'Cookies/*.binarycookies'
          ];
          var deletes = 0;
          var errToRet = null;
          var finishOne = function (err) {
            deletes++;
            if (err) {
              errToRet = err;
            }
            if (deletes === toDeletes.length) {
              finishAll(errToRet);
            }
          };
          _.each(toDeletes, function (del) {
            var toDelete = path.resolve(baseSupportDir, del);
            toDelete = toDelete.replace(/ /g, "\\ ");
            logger.info("Deleting " + toDelete);
            var cmd = "rm -rf " + toDelete;
            exec(cmd, function (err) {
              finishOne(err);
            });
          });
        });
      });
    }
  });
};

exports.getUser = function (cb) {
  logger.info("Determining current user");
  exec("whoami", { maxBuffer: 524288 }, function (err, stdout) {
    if (err) {
      logger.error(err);
      cb(err);
    } else {
      logger.info("User is " + stdout.trim());
      cb(null, stdout.trim());
    }
  });
};

exports.delay = function (secs) {
  var date = new Date();
  var curDate = null;
  do { curDate = new Date(); }
  while (curDate - date < (secs * 1000.0));
};

// var pad0 = function (x) {
//   if (x.toString().length == 1) {
//     x = '0' + x;
//   }
//   return x;
// };

exports.escapeSpecialChars = function (str, quoteEscape) {
  if (typeof str !== "string") {
    return str;
  }
  if (typeof quoteEscape === "undefined") {
    quoteEscape = false;
  }
  str = str
        .replace(/[\\]/g, '\\\\')
        .replace(/[\/]/g, '\\/')
        .replace(/[\b]/g, '\\b')
        .replace(/[\f]/g, '\\f')
        .replace(/[\n]/g, '\\n')
        .replace(/[\r]/g, '\\r')
        .replace(/[\t]/g, '\\t')
        .replace(/[\"]/g, '\\"')
        .replace(/\\'/g, "\\'");
  if (quoteEscape) {
    var re = new RegExp(quoteEscape, "g");
    str = str.replace(re, "\\" + quoteEscape);
  }
  return str;
};

exports.parseWebCookies = function (cookieStr) {
  var cookies = [];
  var splits = cookieStr.trim().split(";");
  _.each(splits, function (split) {
    split = split.trim();
    if (split !== "") {
      split = split.split("=");
      cookies.push({
        name: decodeURIComponent(split[0])
      , value: decodeURIComponent(split[1])
      });
    }
  });
  return cookies;
};

var warningsEmitted = {};
var deprecationWarnings = [];

var warningText = _.template(
  "[DEPRECATED] The <%= deprecated %> <%= kind %> has been deprecated and will " +
  "be removed."
);

var replacementText = _.template(
  "  Please use the <%= replacement %> <%= kind %> instead."
);

exports.formatDeprecationWarning = function (kind, deprecated, replacement) {
  var warning = warningText({kind: kind, deprecated: deprecated});
  if (typeof replacement !== 'undefined') {
    warning += replacementText({kind: kind, replacement: replacement});
  }
  return warning;
};

exports.logDeprecationWarning = function (kind, deprecated, replacement) {
  if (!_.has(warningsEmitted, kind)) {
    warningsEmitted[kind] = {};
  }
  if (!_.has(warningsEmitted[kind], deprecated)) {
    var warning = exports.formatDeprecationWarning(kind, deprecated, replacement);
    deprecationWarnings.push(warning);
    logger.warn(warning);
    warningsEmitted[kind][deprecated] = true;
  }
};

exports.logCustomDeprecationWarning = function (kind, deprecated, msg) {
  if (!_.has(warningsEmitted, kind)) {
    warningsEmitted[kind] = {};
  }
  if (!_.has(warningsEmitted[kind], deprecated)) {
    deprecationWarnings.push(msg);
    logger.warn(msg);
    warningsEmitted[kind][deprecated] = true;
  }
};

exports.logFinalDeprecationWarning = function () {
  var numWarnings = deprecationWarnings.length;
  if (numWarnings > 0) {
    logger.warn("[DEPRECATED] You used " + numWarnings + " deprecated" +
                " capabilities during this session.  Please check the logs" +
                " as they will be removed in a future version of Appium.");
  }
};

exports.getDeprecationWarnings = function () {
  return deprecationWarnings;
};

exports.clearWarnings = function () {
  warningsEmitted = {};
  deprecationWarnings = [];
};

exports.rotateImage = function (imgPath, deg, cb) {
  logger.info("Rotating image " + imgPath + " " + deg + " degrees");
  var scriptPath = require('appium-uiauto').rotate;
  var cmd = "osascript " + scriptPath + " " + JSON.stringify(imgPath) +
            " " + deg;
  exec(cmd, { maxBuffer: 524288 }, function (err, stdout) {
    if (err) return cb(err);
    console.log(stdout);
    cb(null);
  });
};

exports.isWindows = function () {
  return osType === 'Windows_NT';
};

exports.isMac = function () {
  return osType === 'Darwin';
};

exports.isLinux = function () {
  return !exports.isWindows() && !exports.isMac();
};

exports.macVersionArray = function (cb) {
  var versions = [];
  if (exports.isMac()) {
    exec("sw_vers -productVersion", function (err, stdout) {
      if (err) return cb(err);
      stdout = stdout.trim();
      if (/\d+\.\d+\.\d+/.test(stdout)) {
        _.each(stdout.split("."), function (ver) {
          versions.push(parseInt(ver, 10));
        });
        cb(null, versions);
      } else {
        cb(new Error("Could not parse version information from: " + stdout));
      }
    });
  } else {
    cb(null, versions);
  }
};

exports.getTempPath = function () {
  return exports.isWindows() ? "C:\\Windows\\Temp" : "/tmp";
};

exports.getAppsBackupPath = function () {
  if (process.env.APPIUM_APP_BACKUP_DIR) return process.env.APPIUM_APP_BACKUP_DIR;
  return exports.getTempPath();
};

exports.getGitRev = function (cb) {
  var cwd = path.resolve(__dirname, "..");
  exec("git rev-parse HEAD", {cwd: cwd, maxBuffer: 524288}, function (err, stdout) {
    if (err) return cb(err);
    cb(null, stdout.trim());
  });
};

exports.getAndroidPlatform = function () {
  var androidHome = process.env.ANDROID_HOME;
  if (typeof androidHome !== "string") {
    logger.error("ANDROID_HOME was not exported!");
    return null;
  }

  var locs = ['android-4.2', 'android-17', 'android-4.3', 'android-18',
      'android-4.4', 'android-19'];
  var res = null;
  _.each(locs.reverse(), function (loc) {
    var platforms = path.resolve(androidHome, 'platforms')
    , platform = loc;
    if (res === null && fs.existsSync(path.resolve(platforms, platform))) {
      res = [platform, path.resolve(platforms, platform)];
    }
  });
  return res;
};

exports.getXcodeFolder = function (cb) {
  exec('xcode-select --print-path', { maxBuffer: 524288, timeout: 3000 }, function (err, stdout, stderr) {
    if (!err) {
      var xcodeFolderPath = stdout.replace("\n", "");
      if (fs.existsSync(xcodeFolderPath)) {
        cb(null, xcodeFolderPath);
      } else {
        logger.error(xcodeFolderPath + "does not exist.");
        cb(new Error("xcode-select could not find xcode"), null);
      }
    } else {
      logger.error("xcode-select threw error " + err);
      logger.error("Stderr: " + stderr);
      logger.error("Stdout: " + stdout);
      cb(new Error("xcode-select threw an error"), null);
    }
  });
};

exports.getXcodeVersion = function (cb) {
  exports.getXcodeFolder(function (err, xcodePath) {
    if (err  !== null) {
      cb(err, null);
    } else {
      var xcodebuildPath = path.resolve(xcodePath, "usr/bin/xcodebuild");
      if (!fs.existsSync(xcodebuildPath)) {
        cb(new Error("Could not get Xcode version: " + xcodebuildPath + " does not exist on disk."), null);
      } else {
        exec(JSON.stringify(xcodebuildPath) + ' -version', { maxBuffer: 524288, timeout: 3000 }, function (err, stdout) {
          var versionPattern = /\d\.\d\.*\d*/;
          var match = versionPattern.exec(stdout);
          if (match === null) {
            cb(new Error("Could not parse Xcode version"), null);
          } else {
            var versionNumber = match[0];
            cb(null, versionNumber);
          }
        });
      }
    }
  });
};

exports.getiOSSDKVersion = function (cb) {
  var msg;
  exports.getXcodeVersion(function (err, versionNumber) {
    if (err) {
      msg = "Could not get the iOS SDK version because the Xcode version could not be determined.";
      logger.error(msg);
      cb(new Error(msg), null);
    } else if (versionNumber[0] === '4') {
      cb(null, '6.1');
    } else {
      exec('xcrun --sdk iphonesimulator --show-sdk-version', { maxBuffer: 524288, timeout: 3000 }, function (err, stdout, stderr) {
        if (!err) {
          var iosSDKVersion = stdout.replace("\n", "");
          var match = /\d.\d/.exec(iosSDKVersion);
          if (match) {
            cb(null, iosSDKVersion);
          } else {
            msg = "xcrun returned a non-numeric iOS SDK version: " + iosSDKVersion;
            logger.error(msg);
            cb(new Error(msg), null);
          }
        } else {
          msg = "xcrun threw an error " + err;
          logger.error(msg);
          logger.error("Stderr: " + stderr);
          logger.error("Stdout: " + stdout);
          cb(new Error(msg), null);
        }
      });
    }
  });
};

exports.getiOSSimulatorDirectories = function (cb) {
  var basePath = path.resolve(process.env.HOME, "Library", "Application Support",
    "iPhone Simulator");
  glob(basePath + "/*.*", function (err, pathList) {
    if (err) {
      var msg = "Could not find any directories for the iOS Simulator";
      logger.error(msg);
      cb(new Error(msg));
    } else {
      cb(null, pathList);
    }
  });
};

exports.getAppiumConfig = function () {
  var configPath = path.resolve(__dirname, "..", ".appiumconfig.json");
  var config
    , msg;
  try {
    config = require(configPath);
  } catch (e) {
    if (e.code === "MODULE_NOT_FOUND") {
      msg = "Could not find config file: " + configPath +
                 "; looks like config hasn't been run." +
                 " Please run reset.sh or appium configure.";
      logger.error(msg);
      throw new Error(msg);
    } else {
      msg = "Invalid config file at " + configPath +
                " please re-run reset.sh or appium config";
      logger.error(msg);
      throw new Error(msg);
    }
  }

  return config;
};

exports.iosConfigured = function () {
  return typeof exports.getAppiumConfig().ios !== "undefined";
};

exports.truncateDecimals = function (number, digits) {
  var multiplier = Math.pow(10, digits),
      adjustedNum = number * multiplier,
      truncatedNum = Math[adjustedNum < 0 ? 'ceil' : 'floor'](adjustedNum);

  return truncatedNum / multiplier;
};
