import {get, cloneDeep, isNil, isFunction, isString, isArray, isEqual, isEmpty} from "lodash";

import {InsufficientDataError, WrongInterfaceError, InvalidParamsError, DuplicateError} from "../lib/baseErrors";

let isStoreLinked = false;
export function linkStore(globalStore) {
  if (isStoreLinked) {
    throw new DuplicateError("Attempt to link store twice");
  }

  isStoreLinked = true;

  for (const module of modulesList) {
    module.__initializeMdl(globalStore);
  }
}

export function unlinkStore() {
  if (process.env.NODE_ENV === "production") {
    const msg = "It`s not possible to unlink a store on production. It may cause unpredictable issues related to" +
      " dependency breaks";
    throw new WrongInterfaceError(msg);
  }

  if (!isStoreLinked) {
    throw new InsufficientDataError("Attempt to unlink not linked store");
  }

  isStoreLinked = false;

  for (const module of modulesList) {
    module.__deinitialize();
  }
}

export class RMCCtl {
  constructor() {
    this.__writable = false;

    let ownProperty;
    Object.defineProperty(this, "ownState", {
      get() {
        if (isNil(this.__storeCtl)) {
          return;
        }

        return cloneDeep(ownProperty);
      },
      set(value) {
        if (!this.__writable) {
          throw new WrongInterfaceError("Attempt to set ownState. This property changes via dispatching actions only.");
        }

        ownProperty = value;
      },
    });
  }

  __stateChangeListeners = new Set();

  __initializeCtl(store, path) {
    this.__storeCtl = store;
    this.__pathCtl = path;

    const ownState = this.__getOwnStateCtl();
    this.__setOwnStateCtl(ownState);

    this.__unsubscribeStoreCtl = this.__storeCtl.subscribe(() => {
      const state = this.__storeCtl.getState();

      this.__stateUpdateListenerCtl(state);
    });

    if (isFunction(this._didLinkedWithStore)) {
      this._didLinkedWithStore();
    }
  }

  __deinitializeCtl() {
    this.__unsubscribeStoreCtl();

    this.__storeCtl = undefined;

    if (isFunction(this._didUnlinkedWithStore)) {
      this._didUnlinkedWithStore();
    }
  }

  __getOwnStateCtl(outerState = this.__storeCtl.getState()) {
    const path = this.__pathCtl;

    return get(outerState, path);
  }

  __setOwnStateCtl = state => {
    this.__writable = true;

    this.ownState = state;

    this.__writable = false;
  };

  __stateUpdateListenerCtl(nextOuterState) {
    const prevOwnState = this.ownState;

    const ownState = this.__getOwnStateCtl(nextOuterState);

    this.__setOwnStateCtl(ownState);

    if (isEqual(ownState, prevOwnState)) {
      return;
    }

    if (isFunction(this._stateDidUpdate)) {
      this._stateDidUpdate(prevOwnState);
    }

    this.__stateChangeListeners.forEach(listener => {
      listener(prevOwnState, ownState);
    });
  }

  __unsubscribeCtl(listener) {
    this.__stateChangeListeners.delete(listener);
  }

  subscribe(listener) {
    if (!isFunction(listener)) {
      throw new InvalidParamsError("Attempt to subscribe, but listener is not a function");
    }

    this.__stateChangeListeners.add(listener);

    return this.__unsubscribeCtl.bind(this, listener);
  }

  dispatch(action) {
    if (isNil(this.__storeCtl)) {
      throw new WrongInterfaceError("Can not dispatch while store is not linked");
    }

    this.__storeCtl.dispatch(action);
  }
}

const deprecatedMethodsForCtl = [
  "__validateControllerMdl",
  "__initializeMdl",
  "__pathMdl",
  "__reducerMdl",
  "__controllerMdl",
  "__deinitialize",
];
const warnMethodsForCtl = ["integrator"];
class Module {
  constructor(reducer, CtlClass) {
    if (!isFunction(reducer)) {
      const msg = "Attempt to create a module, but reducer is not a function";

      throw new InvalidParamsError(msg);
    }

    if (!(CtlClass.prototype instanceof RMCCtl)) {
      const msg = `Attempt to create a module with a wrong ctl class "${CtlClass.name}"`;

      throw new InvalidParamsError(msg);
    }

    this.__pathMdl = undefined;
    this.__reducerMdl = reducer;
    this.__controllerMdl = new CtlClass();

    this.__validateControllerMdl(this.__controllerMdl);
  }

  __validateControllerMdl(ctl) {
    let ctlName = Object.getPrototypeOf(ctl).constructor.name;

    for (const methodName of deprecatedMethodsForCtl) {
      if (methodName in ctl) {
        const msg = `A "${ctlName}" contains a deprecated for a controller method/property "${methodName}"`;

        throw new DuplicateError(msg);
      }
    }

    for (const methodName of warnMethodsForCtl) {
      if (methodName in ctl) {
        const msg = `A "${ctlName}" contains a method/property "${methodName}" that exists in the module. You will not have access to the controllers method.`;

        console.warn(msg);
      }
    }
  }

  __initializeMdl(store) {
    this.__controllerMdl.__initializeCtl(store, this.__pathMdl);
  }

  __deinitialize() {
    this.__controllerMdl.__deinitializeCtl();
  }

  integrator(path) {
    const prevPath = this.__pathMdl;

    if (isNil(prevPath)) {
      if (
        (!isString(path) || path === "") &&
        (!isArray(path) || isEmpty(path) || path.some(pathPart => !isString(pathPart)))
      ) {
        throw new InvalidParamsError(`Attempt to integrate bad path: "${path}"`);
      }

      this.__pathMdl = path;
    } else if (!isEqual(prevPath, path)) {
      const msg = `Attempt to change a path of integration: "${prevPath}" -> "${path}"`;

      throw new InvalidParamsError(msg);
    }

    return this.__reducerMdl;
  }
}

const modulesList = [];

export function createModule(reducer, Controller) {
  const module = new Module(reducer, Controller);

  const proxy = new Proxy(module, {
    get(target, propName) {
      if (this._useTarget(target, propName)) {
        return target[propName];
      }

      const result = target.__controllerMdl[propName];
      if (isFunction(result)) {
        return result.bind(target.__controllerMdl);
      } else {
        return result;
      }
    },

    set(target, propName, value) {
      if (this._useTarget(target, propName)) {
        target[propName] = value;
      } else {
        target.__controllerMdl[propName] = value;
      }

      return true;
    },

    _useTarget(target, propName) {
      const isProtected = propName[0] === "_";
      const isPrivate = isProtected && propName[1] === "_";

      return (isPrivate || !isProtected) && propName in target;
    },
  });

  modulesList.push(proxy);

  return proxy;
}

@@ -506,18 +506,22 @@
 e() %7B%0A  
-if
+switch
  (proces
@@ -534,20 +534,28 @@
 NODE_ENV
- ===
+) %7B%0A    case
  %22produc
@@ -559,24 +559,26 @@
 duction%22
-)
+:
  %7B%0A    
+ 
+ 
 const ms
@@ -687,16 +687,18 @@
 +%0A      
+  
 %22 depend
@@ -711,16 +711,19 @@
 reaks%22;%0A
+%0A  
     thro
@@ -754,16 +754,171 @@
 r(msg);%0A
+    %7D%0A%0A    case %22development%22: %7B%0A      console.warn(%22The deprecated method unlinkStore was called. It will cause an exception in production mode.%22);%0A    %7D%0A
   %7D%0A%0A  i

Add notification about the unlinkStore is deprecated for development mode.