/**
 *  Copyright (C) 2018 Basalt
    This file is part of Knapsack.
    Knapsack 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.

    Knapsack 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 Knapsack; if not, see <https://www.gnu.org/licenses>.
 */
import fs from 'fs-extra';
import os from 'os';
import qs from 'qs';
import yaml from 'js-yaml';
import resolve from 'resolve';
import { execSync } from 'child_process';
import prettier from 'prettier';
import { relative, join, isAbsolute } from 'path';
import * as log from '../cli/log';

/**
 * @param fileName - path to where JSON file should be written
 * @param object - data to turn to JSON
 */
export function writeJson(fileName: string, object: object): Promise<void> {
  return fs.writeFile(fileName, JSON.stringify(object, null, 2) + os.EOL);
}

/**
 * @param fileName - path to where JSON file should be read
 */
export function readJson(fileName: string): Promise<{ [k: string]: any }> {
  return fs.readFile(fileName, 'utf8').then(file => JSON.parse(file));
}

export function readJsonSync(fileName: string): object {
  return JSON.parse(fs.readFileSync(fileName, 'utf8'));
}

export function writeYaml(fileName: string, object: object): Promise<void> {
  return fs.writeFile(fileName, yaml.safeDump(object, { noRefs: true }));
}

export function readYaml(fileName: string): Promise<{ [k: string]: any }> {
  return fs
    .readFile(fileName, 'utf8')
    .then(file => yaml.safeLoad(file, { filename: fileName }));
}

export function readYamlSync(fileName: string): object {
  return yaml.safeLoad(fs.readFileSync(fileName, 'utf8'), {
    filename: fileName,
  });
}

export function isRemoteUrl(url: string): boolean {
  return url.startsWith('http') || url.startsWith('//');
}

/**
 * Get a NPM package's package.json as object
 */
export function getPkg(pkg: string): object {
  const pkgPath = require.resolve(`${pkg}/package.json`);
  return readJsonSync(pkgPath);
}

export function fileExists(filePath: string): boolean {
  try {
    return fs.statSync(filePath).isFile();
  } catch (err) {
    return false;
  }
}

export function dirExists(dirPath: string): boolean {
  try {
    return fs.statSync(dirPath).isDirectory();
  } catch (err) {
    return false;
  }
}

export function fileExistsOrExit(filePath: string, msg?: string): void {
  if (fileExists(filePath)) return;
  log.error(msg || `This file does not exist! ${filePath}`);
  process.exit(1);
}

export function dirExistsOrExit(dirPath: string, msg?: string): void {
  if (dirExists(dirPath)) return;
  log.error(msg || `This folder does not exist! ${dirPath}`);
  process.exit(1);
}

export function getGitBranch(): string {
  try {
    const branch = execSync('git symbolic-ref --short HEAD', {
      encoding: 'utf8',
    });
    return branch?.trim();
  } catch (err) {
    console.error(`Uh oh! error thrown in getGitBranch: ${err.message}`);
    return '';
  }
}

export function resolvePath({
  path,
  resolveFromDirs = [],
}: {
  path: string;
  resolveFromDirs?: string[];
}): {
  exists: boolean;
  absolutePath?: string;
  relativePathFromCwd?: string;
  originalPath: string;
  type: 'absolute' | 'relative' | 'package' | 'unknown';
} {
  if (isAbsolute(path)) {
    return {
      originalPath: path,
      absolutePath: path,
      exists: fileExists(path),
      relativePathFromCwd: relative(process.cwd(), path),
      type: 'absolute',
    };
  }
  let absolutePath: string;
  try {
    absolutePath = resolve.sync(path, {
      basedir: process.cwd(),
    });
    if (absolutePath) {
      return {
        originalPath: path,
        exists: true,
        absolutePath,
        relativePathFromCwd: relative(process.cwd(), absolutePath),
        type: 'package',
      };
    }
  } catch (e) {
    // oh well
  }
  let result: {
    exists: boolean;
    absolutePath?: string;
    relativePathFromCwd?: string;
    originalPath: string;
    type: 'relative';
  };
  resolveFromDirs.forEach(base => {
    const x = join(process.cwd(), join(base, path));
    if (fileExists(x)) {
      result = {
        exists: true,
        absolutePath: x,
        originalPath: path,
        relativePathFromCwd: relative(process.cwd(), x),
        type: 'relative',
      };
    }
  });
  if (result?.exists) return result;

  return {
    exists: false,
    originalPath: path,
    type: 'unknown',
  };
}

/**
 * Parse QueryString, decode non-strings
 * Changes strings like `'true'` to `true` among others like numbers
 * @see qsStringify
 */
export function qsParse(querystring: string): object {
  return qs.parse(querystring, {
    // This custom decoder is for turning values like `foo: "true"` into `foo: true`, along with Integers, null, and undefined.
    // https://github.com/ljharb/qs/issues/91#issuecomment-437926409
    decoder(str) {
      const strWithoutPlus = str.replace(/\+/g, ' ');

      if (/^(\d+|\d*\.\d+)$/.test(str)) {
        return parseFloat(str);
      }

      const keywords = {
        true: true,
        false: false,
        null: null,
        undefined,
      };
      if (str in keywords) {
        return keywords[str];
      }

      // utf8
      try {
        return decodeURIComponent(strWithoutPlus);
      } catch (e) {
        return strWithoutPlus;
      }
    },
  });
}

/**
 * Turn object of data into query string
 * @see qsParse
 */
export function qsStringify(data: object): string {
  return qs.stringify(data);
}

export function base64ToString(b64: string): string {
  return Buffer.from(b64, 'base64').toString();
}

export function stringToBase64(string: string): string {
  return Buffer.from(string).toString('base64');
}

/**
 * Format code with Prettier
 * If it can't format, it just returns original code
 * @link https://prettier.io/docs/en/options.html#parser
 */
export function formatCode({
  code,
  language,
}: {
  code: string;
  language: string;
}): string {
  if (!code) return code;
  try {
    const format = parser =>
      prettier.format(code?.trim(), {
        trailingComma: 'all',
        singleQuote: true,
        semi: true,
        parser,
        htmlWhitespaceSensitivity: 'ignore',
      });

    switch (language) {
      case 'html':
        return format('html');
      case 'react':
      case 'js':
      case 'jsx':
        return format('babel');
      case 'ts':
      case 'tsx':
      case 'react-typescript':
        return format('typescript');
      case 'json':
        return format('json');
      case 'yml':
      case 'yaml':
        return format('yaml');
      case 'md':
      case 'markdown':
        return format('markdown');
      default:
        return code?.trim();
    }
  } catch (error) {
    console.error(error);
    return code;
  }
}
