const parseJson = require('parse-json');
const { cachePrefix } = require('.');

class ParityRoot {
  constructor() {
    this.children = [];
  }

  add(name) {
    const bits = new ParityCache(name);
    this.children.push(bits);
    return bits;
  }

  verify() {
    const firstChild = this.children[0];
    if (!this.children.some(child => child.root)) {
      return true;
    }
    for (const child of this.children) {
      if (!child.verify()) {
        this.reason = {
          cache: child,
          cacheName: child.name,
          cacheReason: child.reason,
          message: `Cache ${child.name} is not complete. ${
            child.reason.message
          }`,
        };
        return false;
      }
      if (child !== firstChild && child.root.token !== firstChild.root.token) {
        this.reason = {
          firstCache: firstChild,
          firstCacheName: firstChild.name,
          firstCacheReason: firstChild.reason,
          secondCache: child,
          secondCacheName: child.name,
          secondCacheReason: child.reason,

          message: `Cache ${firstChild.name} and ${child.name} disagree.`,
        };
        return false;
      }
    }
    return true;
  }
}

class ParityCache {
  constructor(name) {
    this.name = name;
    this.root = null;
    this.bits = {};

    this.reason = null;
  }

  add(_token) {
    const token = ParityToken.fromJson(_token);
    if (token.isRoot) {
      this.root = token;
    }
    this.bits[token.id] = token;
  }

  verify() {
    if (this.root === null) {
      this.reason = {
        message: 'Root compilation not found.',
      };
      return false;
    }

    for (const id of this.root.ids) {
      if (typeof this.bits[id] === 'undefined') {
        this.reason = {
          message: `Compilation '${id}' not found.`,
        };
        return false;
      } else if (this.root.token !== this.bits[id].token) {
        this.reason = {
          message: `Root and '${id}' compilation disagree.`,
        };
        return false;
      }
    }

    return true;
  }
}

const createParityToken = (id, ids = null) => {
  const token = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c =>
    c === 'x'
      ? ((Math.random() * 16) | 0).toString(16)
      : (((Math.random() * 4) | 0) + 8).toString(16),
  );

  return new ParityToken(id, token, ids);
};

class ParityToken {
  constructor(id, token, ids = null) {
    this.id = id;
    this.token = token;

    this.isRoot = ids !== null;
    this.ids = ids;
  }

  static fromCompilation(compilation) {
    let parentCompilation = compilation;
    while (parentCompilation.compiler.parentCompilation) {
      parentCompilation = parentCompilation.compiler.parentCompilation;
    }

    if (!parentCompilation.__hardSource_parityToken) {
      parentCompilation.__hardSource_parityToken = createParityToken(
        cachePrefix(parentCompilation),
        [],
      );
    }

    if (compilation !== parentCompilation) {
      return parentCompilation.__hardSource_parityToken.createChild(
        cachePrefix(compilation),
      );
    }
    return parentCompilation.__hardSource_parityToken;
  }

  static fromJson(json) {
    return new ParityToken(json.id, json.token, json.ids);
  }

  createChild(id) {
    this.ids.push(id);

    return new ParityToken(id, this.token);
  }

  toJSON() {
    return {
      type: 'CacheParityToken',
      id: this.id,
      ids: this.ids,
      token: this.token,
    };
  }
}

const parseIfString = item => {
  if (typeof item === 'string') {
    return parseJson(item);
  }
  return item;
};

const parityCacheFromCache = (name, parityRoot, cache) => {
  const parityCache = parityRoot.add(name);
  if (cache.__hardSource_parityToken_root) {
    const rootCompilation = parseIfString(cache.__hardSource_parityToken_root);
    parityCache.add(rootCompilation);
    rootCompilation.ids.forEach(id => {
      if (cache[`__hardSource_parityToken_${id}`]) {
        parityCache.add(parseIfString(cache[`__hardSource_parityToken_${id}`]));
      }
    });
  }
};

const pushParityWriteOps = (compilation, ops) => {
  if (compilation.compiler.parentCompilation) {
    ops.push({
      key: `__hardSource_parityToken_${cachePrefix(compilation)}`,
      value: JSON.stringify(ParityToken.fromCompilation(compilation)),
    });
  } else {
    ops.push({
      key: `__hardSource_parityToken_root`,
      value: JSON.stringify(ParityToken.fromCompilation(compilation)),
    });
  }
};

module.exports = {
  ParityRoot,
  ParityCache,
  ParityToken,

  parityCacheFromCache,
  pushParityWriteOps,
};
