const path = require("path");
const chalk = require("chalk");
const fs = require("fs");
const jsonc = require("jsonc");
const glob = require("glob");

class AssetsHelper {
  constructor(dirname) {
    this.dirname = dirname;
  }

  getReturnObject(files, condition = "*") {
    return [
      {
        files,
        condition,
      },
    ];
  }

  assets(files, dir = "assets", condition = "*") {
    return this.getReturnObject(
      files.map((file) => path.resolve(this.dirname, dir + "/" + file)),
      condition
    );
  }

  plugin(dir, entryFile = "webpack.entry", condition = "*") {
    // return require(path.resolve(this.dirname, "./inoby-plugins/" + dir + "/" + entryFile));
    return this.getReturnObject(
      require(path.resolve(
        this.dirname,
        "./inoby-plugins/" + dir + "/" + entryFile
      )),
      condition
    );
  }

  post_type(dir, condition = "*") {
    return this.getReturnObject(
      require(path.resolve(
        this.dirname,
        "./post-types/" + dir + "/webpack.entry"
      )),
      condition
    );
  }

  template(dir, condition = "*") {
    return this.getReturnObject(
      require(path.resolve(
        this.dirname,
        "./page-templates/" + dir + "/webpack.entry"
      )),
      condition
    );
  }

  taxonomy(dir, condition = "*") {
    return this.getReturnObject(
      require(path.resolve(
        this.dirname,
        "./taxonomies/" + dir + "/webpack.entry"
      )),
      condition
    );
  }

  checkComponentAllowed(cfg, dir) {
    return (
      cfg.allowed_blocks == null ||
      cfg.allowed_blocks.includes(`meta-box/${dir}`) ||
      cfg.allowed_blocks.includes(`meta-box/inoby-${dir}`)
    );
  }

  component(dir, condition = "*") {
    if (condition == "allowed_blocks") {
      condition = (cfg) => this.checkComponentAllowed(cfg, dir);
    }

    return this.getReturnObject(
      require(path.resolve(
        this.dirname,
        "./components/" + dir + "/webpack.entry"
      )),
      condition
    );
  }

  entry(dir, entryName = "webpack.entry") {
    return require(path.resolve(this.dirname, dir, entryName));
  }

  componentConfigs() {
    const compDir = path.resolve(this.dirname, "./components");
    const configs = glob.sync(`${compDir}/**/config.json*`);

    return this.getReturnObject(configs, "*");
  }
}

class ConfigLoader {
  static load(files) {
    return files.map((file) => {
      let content = jsonc.readSync(file, { stripComments: true });
      if (/[\/\\]components[\/\\]/g.test(file)) {
        return {
          components: content,
        };
      }
      return content;
    });
  }
}

class MergeHelper {
  static merge(defs) {
    let result = {};
    defs.forEach((def) => {
      Object.entries(def).forEach(([key, value]) => {
        if (!result[key]) {
          result[key] = def[key];
        } else if (typeof value !== "object") {
          // jednoduche typy sa prepisuju
          result[key] = value;
        } else if (Array.isArray(value)) {
          // polia sa merguju
          result[key] = [...result[key], ...def[key]];
        } else {
          // objekty posielam dalej
          result[key] = this.merge([result[key], def[key]]);
        }
      });
    });
    return result;
  }
}

class BuildHelper {
  constructor() {
    this.def = MergeHelper.merge(Object.values(arguments));
    this.config = this.getConfig();
  }

  get_asset_keys(type, uris, result = {}) {
    let css = [];
    let js = [];
    let json = [];

    uris.forEach((uri) => {
      // check for nested array
      if (Array.isArray(uri)) {
        result = this.get_asset_keys(type, uri, result);
      } else if (/\.(scss|sass|css)$/.test(uri)) {
        css.push(uri);
      } else if (/\.(js|ts|tsx)$/.test(uri)) {
        js.push(uri);
      } else if (/\.(json|jsonc)$/.test(uri)) {
        json.push(uri);
      }
    });

    if (js.length) {
      result["js/" + type] = js;
    }
    if (css.length) {
      result["css/" + type] = css;
    }
    if (json.length) {
      result[type] = json;
    }

    return result;
  }

  get_assets(type) {
    let assetKeys = {};

    this.def[type].forEach((entry) => {
      const entryKeys = this.checkCondition(entry.condition)
        ? this.get_asset_keys(type, entry.files)
        : {};
      assetKeys = MergeHelper.merge([assetKeys, entryKeys]);
    });

    return assetKeys;
  }

  get_array_assets(type) {
    return Object.entries(this.def[type]).reduce((result, [key, entries]) => {
      let assetKeys = {};

      entries.forEach((entry) => {
        const entryKeys = this.checkCondition(entry.condition)
          ? this.get_asset_keys(type + "/" + key, entry.files)
          : {};
        assetKeys = MergeHelper.merge([assetKeys, entryKeys]);
      });

      return {
        ...result,
        ...assetKeys,
      };
    }, {});
  }

  checkCondition(condition) {
    // always include condition / config not supported
    if (condition === "*" || this.config == null) {
      return true;
    }

    if (typeof condition === "function") {
      return condition(this.config);
    }

    const andConds = condition.split("&&");
    const orConds = condition.split("||");

    const makeSafe = (cond) =>
      cond.trim().replace(/\?\./g, ".").replace(/\./g, "?.");
    const conditionError = (cond, err) => {
      console.warn(chalk.red(`Chyba v podmienke [${cond}]`));
      console.error("Error: ", err);
      return false;
    };
    const isObject = (val) =>
      typeof val === "object" && !Array.isArray(val) && val !== null;
    const checkObjectContainsTrue = (obj) => {
      for (const v of Object.values(obj)) {
        //check nested object
        if (isObject(v)) {
          if (checkObjectContainsTrue(v) === true) {
            return true;
          }
        }

        // check if value is true/string/number etc.
        if (!!v) {
          return true;
        }
      }

      return false;
    };
    const evalCondition = (cond) => {
      try {
        const res = eval(`this.config?.${makeSafe(cond)}`);
        /**
         * Check if result of eval is object
         * ----------------------
         * "some_condition": {
         *    datepicker: false,
         *    capacity: false
         * }
         * ----------------------
         *
         * If condition is only "some_condition", then
         * we should evaluate this condition as true only when
         * the object contains some value equal to "true"
         */
        if (isObject(res)) {
          return checkObjectContainsTrue(res);
        }
        // cast result to boolean and return
        return !!res;
      } catch (error) {
        return conditionError(cond, error);
      }
    };

    // only 1 condition
    if (andConds.length == 1 && orConds.length == 1) {
      return evalCondition(condition);
    }

    // and relation
    if (andConds.length > orConds.length) {
      for (const cond of andConds) {
        if (!evalCondition(cond)) {
          return false;
        }
      }
      return true;
    }

    // or relation
    if (orConds.length > andConds.length) {
      for (const cond of orConds) {
        if (evalCondition(cond)) {
          return true;
        }
      }
      return false;
    }

    // safe return
    return false;
  }

  build() {
    const entryObj = Object.entries(this.def).reduce((result, [key, value]) => {
      return {
        ...result,
        ...(Array.isArray(value)
          ? this.get_assets(key)
          : this.get_array_assets(key)),
      };
    }, {});

    return entryObj;
  }

  getConfig() {
    if (global.inobyConfig) {
      return global.inobyConfig;
    }
    let def = {};

    // check if child theme
    if (path.basename(process.cwd()) !== "inoby-theme") {
      // check child theme version
      const { version } = require(path.resolve(
        process.cwd(),
        "./package.json"
      ));
      const [major, minor, patch] = version.split(".");
      // inoby-config is supported from version 1 above
      if (major && major < 1 && !global.inobyConfigWarningShown) {
        console.warn(
          `
${chalk.bgCyanBright(" Inoby update ")}
------------------------------------------------------
Táto verzia témy nepodporuje inoby_config vo webpacku.
Viac informácií: https://gitlab.com/inoby/inoby-wiki/-/blob/master/common/inoby-theme/webpack-inoby-config.md
------------------------------------------------------
          `
        );
        global.inobyConfigWarningShown = true;
        return null;
      }

      const childDef = require(path.resolve(process.cwd(), "./webpack.entry"));
      const parentDef = require(path.resolve(
        process.cwd(),
        "../inoby-theme/webpack.entry"
      ));

      def = MergeHelper.merge([parentDef, childDef]);
    } else {
      def = require(path.resolve(process.cwd(), "./webpack.entry"));
    }

    let configs = [];

    def.config.forEach((config) => {
      configs = [...configs, ...ConfigLoader.load(config.files)];
    });

    global.inobyConfig = MergeHelper.merge(configs);

    return global.inobyConfig;
  }
}

module.exports = {
  AssetsHelper: AssetsHelper,
  BuildHelper: BuildHelper,
  MergeHelper: MergeHelper,
  ConfigLoader: ConfigLoader,
};
