var IS_TEST_MODE = !!process.env.IS_TEST_MODE;
var Board = require("./board");
var Expander = require("./expander.js");
var EVS = require("./evshield");
var __ = require("./fn");
var events = require("events");
var util = require("util");
var Collection = require("./mixins/collection");
var Sensor = require("./sensor");
var ShiftRegister = require("./shiftregister");

var priv = new Map();
var registers = new Map();

function registerKey(registerOpts) {
  return ["clock", "data", "latch"].reduce(function(accum, key) {
    return accum + "." + registerOpts[key];
  }, "");
}

function latch(state, bit, on) {
  return on ? state |= (1 << bit) : state &= ~(1 << bit);
}

function updateShiftRegister(motor, dir) {
  var rKey = registerKey(motor.opts.register),
    register = registers.get(motor.board)[rKey],
    latchState = register.value,
    bits = priv.get(motor).bits,
    forward = dir !== "reverse";

  // There are two ShiftRegister bits which we need to change based on the
  // direction of the motor.  These will be the pins that control the HBridge
  // on the board.  They will get flipped high/low based on the current flow
  // in the HBridge.
  latchState = latch(latchState, bits.a, forward);
  latchState = latch(latchState, bits.b, !forward);

  if (register.value !== latchState) {
    register.send(latchState);
  }
}

var Controllers = {
  ShiftRegister: {
    initialize: {
      value: function(opts) {
        var rKey = registerKey(opts.register);

        if (!opts.bits || opts.bits.a === undefined || opts.bits.b === undefined) {
          throw new Error("ShiftRegister Motors MUST contain HBRIDGE bits {a, b}");
        }

        priv.get(this).bits = opts.bits;

        if (!registers.has(this.board)) {
          registers.set(this.board, {});
        }

        if (!registers.get(this.board)[rKey]) {
          registers.get(this.board)[rKey] = new ShiftRegister({
            board: this.board,
            pins: opts.register
          });
        }

        this.io.pinMode(this.pins.pwm, this.io.MODES.PWM);
      }
    },
    dir: {
      value: function(dir) {
        this.stop();

        updateShiftRegister(this, dir.name);

        this.direction = dir;

        process.nextTick(this.emit.bind(this, dir.name));

        return this;
      }
    }
  },
  PCA9685: {
    setPWM: {
      writable: true,
      value: function(pin, speed) {
        var state = priv.get(this);
        state.expander.analogWrite(pin, speed);
      }
    },
    setPin: {
      writable: true,
      value: function(pin, value) {
        var state = priv.get(this);
        state.expander.digitalWrite(pin, value);
      }
    },
    initialize: {
      value: function(opts) {

        var state = priv.get(this);

        this.address = opts.address || 0x40;
        this.pwmRange = opts.pwmRange || [0, 4080];
        this.frequency = opts.frequency || 50;

        state.expander = Expander.get({
          address: this.address,
          controller: this.controller,
          bus: this.bus,
          pwmRange: this.pwmRange,
          frequency: this.frequency,
        });

        Object.keys(this.pins).forEach(function(pinName) {
          this.pins[pinName] = state.expander.normalize(this.pins[pinName]);
        }, this);

      }
    }
  },
  EVS_EV3: {
    initialize: {
      value: function(opts) {
        var state = priv.get(this);

        state.shield = EVS.shieldPort(opts.pin);
        state.ev3 = new EVS(Object.assign(opts, {
          io: this.io
        }));

        this.opts.pins = {
          pwm: opts.pin,
          dir: opts.pin,
        };
      }
    },
    setPWM: {
      value: function(pin, value) {
        var state = priv.get(this);

        var register = state.shield.motor === EVS.M1 ? EVS.SPEED_M1 : EVS.SPEED_M2;
        var speed = __.scale(value, 0, 255, 0, 100) | 0;

        if (value === 0) {
          state.ev3.write(state.shield, EVS.COMMAND, EVS.Motor_Reset);
        } else {
          if (!this.direction.value) {
            speed = -speed;
          }

          var data = [
            // 0-100
            speed,
            // Duration (0 is forever)
            0,
            // Command B
            0,
            // Command A
            EVS.CONTROL_SPEED | EVS.CONTROL_GO
          ];

          state.ev3.write(state.shield, register, data);
        }
      }
    },
    setPin: {
      value: function(pin, value) {
        this.setPWM(this.pin, value);
      }
    },
  },

  GROVE_I2C_MOTOR_DRIVER: {
    REGISTER: {
      value: {
        ADDRESS: 0x0F,
      }
    },
    COMMANDS: {
      value: {
        SET_SPEED: 0x82,
        SET_PWM_FREQUENCY: 0x84,
        SET_DIRECTION: 0xAA,
        NOOP: 0x01,
      }
    },

    initialize: {
      value: function(opts) {
        var state = priv.get(this);
        var shared = priv.get("GROVE_I2C_MOTOR_DRIVER");

        if (!shared) {
          shared = {
            direction: {
              A: 0x01,
              B: 0x01,
            },
            speed: {
              A: 0,
              B: 0,
            }
          };

          priv.set("GROVE_I2C_MOTOR_DRIVER", shared);
        }

        state.shared = shared;
        state.pin = opts.pin.toUpperCase();

        this.opts.pins = {
          pwm: opts.pin,
          dir: opts.pin,
        };

        this.address = opts.address || this.REGISTER.ADDRESS;

        opts.address = this.address;

        this.io.i2cConfig(opts);
      }
    },
    setPWM: {
      value: function(pin, value) {
        var state = priv.get(this);
        var speed = Board.constrain(value, 0, 255) | 0;

        state.shared.speed[state.pin] = speed;

        this.io.i2cWrite(this.address, [
          this.COMMANDS.SET_SPEED,
          state.shared.speed.A,
          state.shared.speed.B,
        ]);
      }
    },
    setPin: {
      value: function(pin, value) {
        var state = priv.get(this);

        // DIR_CCW = 0x02
        // DIR_CW  = 0x01
        state.shared.direction[state.pin] = value ? 0x01 : 0x02;

        var a = state.shared.direction.A & 0x03;
        var b = state.shared.direction.B & 0x03;
        var direction = (b << 2) | a;

        this.io.i2cWrite(this.address, [
          this.COMMANDS.SET_DIRECTION,
          direction,
          this.COMMANDS.NOOP,
        ]);
      }
    }
  }
};

// Aliases
//
// NXT motors have the exact same control commands as EV3 motors
Controllers.EVS_NXT = Controllers.EVS_EV3;

var Devices = {
  NONDIRECTIONAL: {
    pins: {
      get: function() {
        return {
          pwm: this.opts.pin
        };
      }
    },
    dir: {
      writable: true,
      configurable: true,
      value: function(speed) {
        speed = speed || this.speed();
        return this;
      }
    },
    resume: {
      value: function() {
        var speed = this.speed();
        this.speed({
          speed: speed
        });
        return this;
      }
    }
  },
  DIRECTIONAL: {
    pins: {
      get: function() {
        if (Array.isArray(this.opts.pins)) {
          return {
            pwm: this.opts.pins[0],
            dir: this.opts.pins[1]
          };
        } else {
          return this.opts.pins;
        }
      }
    },
    dir: {
      writable: true,
      configurable: true,
      value: function(dir) {

        this.stop();

        this.setPin(this.pins.dir, dir.value);
        this.direction = dir;

        process.nextTick(this.emit.bind(this, dir.name));

        return this;
      }
    }
  },
  CDIR: {
    pins: {
      get: function() {
        if (Array.isArray(this.opts.pins)) {
          return {
            pwm: this.opts.pins[0],
            dir: this.opts.pins[1],
            cdir: this.opts.pins[2]
          };
        } else {
          return this.opts.pins;
        }
      }
    },
    dir: {
      value: function(dir) {

        this.stop();
        this.direction = dir;

        this.setPin(this.pins.cdir, 1 ^ dir.value);
        this.setPin(this.pins.dir, dir.value);

        process.nextTick(this.emit.bind(this, dir.name));

        return this;
      }
    },
    brake: {
      value: function(duration) {

        this.speed({
          speed: 0,
          saveSpeed: false
        });
        this.setPin(this.pins.dir, 1, 127);
        this.setPin(this.pins.cdir, 1, 128, 127);
        this.speed({
          speed: 255,
          saveSpeed: false,
          braking: true
        });

        process.nextTick(this.emit.bind(this, "brake"));

        if (duration) {
          var motor = this;
          this.board.wait(duration, function() {
            motor.stop();
          });
        }

        return this;
      }
    }
  }
};

/**
 * Motor
 * @constructor
 *
 * @param {Object} opts Options: pin|pins{pwm, dir[, cdir]}, device, controller, current
 * @param {Number} pin A single pin for basic
 * @param {Array} pins A two or three digit array of pins [pwm, dir]|[pwm, dir, cdir]
 *
 *
 * Initializing "Hobby Motors"
 *
 *    new five.Motor(9);
 *
 * ...is the same as...
 *
 *    new five.Motor({
 *      pin: 9
 *    });
 *
 *
 * Initializing 2 pin, Bi-Directional DC Motors:
 *
 *    new five.Motor([ 3, 12 ]);
 *
 * ...is the same as...
 *
 *    new five.Motor({
 *      pins: [ 3, 12 ]
 *    });
 *
 * ...is the same as...
 *
 *    new five.Motor({
 *      pins: {
 *        pwm: 3,
 *        dir: 12
 *      }
 *    });
 *
 *
 * Initializing 3 pin, I2C PCA9685 Motor Controllers:
 * i.e. The Adafruit Motor Shield V2
 *
 *    new five.Motor({
 *      pins: [ 8, 9, 10 ],
 *      controller: "PCA9685",
 *      address: 0x60
 *    });
 *
 *
 * Initializing 3 pin, Bi-Directional DC Motors:
 *
 *    new five.Motor([ 3, 12, 11 ]);
 *
 * ...is the same as...
 *
 *    new five.Motor({
 *      pins: [ 3, 12, 11 ]
 *    });
 *
 * ...is the same as...
 *
 *    new five.Motor({
 *      pins: {
 *        pwm: 3,
 *        dir: 12,
 *        cdir: 11
 *      }
 *    });
 *
 *
 * Initializing Bi-Directional DC Motors with brake:
 *
 *    new five.Motor({
 *      pins: {
 *        pwm: 3,
 *        dir: 12,
 *        brake: 11
 *      }
 *    });
 *
 *
 * Initializing Bi-Directional DC Motors with current sensing pins:
 * See Sensor.js for details on options
 *
 *    new five.Motor({
 *      pins: [3, 12],
 *      current: {
 *        pin: "A0",
 *        freq: 250,
 *        range: [0, 2000]
 *      }
 *    });
 *
 *
 * Initializing Bi-Directional DC Motors with inverted speed for reverse:
 * Most likely used for non-commercial H-Bridge controllers
 *
 *    new five.Motor({
 *      pins: [3, 12],
 *      invertPWM: true
 *    });
 *
 */

function Motor(opts) {

  var device, controller, state;

  if (!(this instanceof Motor)) {
    return new Motor(opts);
  }

  Board.Component.call(
    this, this.opts = Board.Options(opts)
  );

  controller = opts.controller || null;

  // Derive device based on pins passed
  if (typeof this.opts.device === "undefined") {

    this.opts.device = (typeof this.opts.pins === "undefined" && typeof this.opts.register !== "object") ?
      "NONDIRECTIONAL" : "DIRECTIONAL";

    if (this.opts.pins && (this.opts.pins.cdir || this.opts.pins.length > 2)) {
      this.opts.device = "CDIR";
    }

    if (typeof controller === "string" &&
      (controller.startsWith("EVS") || controller.startsWith("GROVE_I2C"))) {
      this.opts.device = "DIRECTIONAL";
    }
  }

  // Allow users to pass in custom device types
  device = typeof this.opts.device === "string" ?
    Devices[this.opts.device] : this.opts.device;

  this.threshold = typeof this.opts.threshold !== "undefined" ?
    this.opts.threshold : 30;

  this.invertPWM = typeof this.opts.invertPWM !== "undefined" ?
    this.opts.invertPWM : false;

  Object.defineProperties(this, device);

  if (this.opts.register) {
    this.opts.controller = "ShiftRegister";
  }

  /**
   * Note: Controller decorates the device. Used for adding
   * special controllers (i.e. PCA9685)
   **/
  if (this.opts.controller) {
    controller = typeof this.opts.controller === "string" ?
      Controllers[this.opts.controller] : this.opts.controller;

    Board.Controller.call(this, controller, opts);
  }

  // current just wraps a Sensor
  if (this.opts.current) {
    this.opts.current.board = this.board;
    this.current = new Sensor(this.opts.current);
  }

  // Create a "state" entry for privately
  // storing the state of the motor
  state = {
    isOn: false,
    currentSpeed: typeof this.opts.speed !== "undefined" ?
      this.opts.speed : 128,
    braking: false,
    enabled: true
  };

  priv.set(this, state);

  Object.defineProperties(this, {
    // Calculated, read-only motor on/off state
    // true|false
    isOn: {
      get: function() {
        return state.isOn;
      }
    },
    currentSpeed: {
      get: function() {
        return state.currentSpeed;
      }
    },
    braking: {
      get: function() {
        return state.braking;
      }
    },
    enabled: {
      get: function() {
        return state.enabled;
      }
    }
  });

  // We need to store and initialize the state of the dir pin(s)
  this.direction = {
    value: 1
  };

  if (this.initialize) {
    this.initialize(opts);
  }

  this.enable();
  this.dir(this.direction);
}

util.inherits(Motor, events.EventEmitter);

Motor.prototype.initialize = function() {
  this.io.pinMode(this.pins.pwm, this.io.MODES.PWM);

  ["dir", "cdir", "brake", "enable"].forEach(function(pin) {
    if (typeof this.pins[pin] !== "undefined") {
      this.io.pinMode(this.pins[pin], this.io.MODES.OUTPUT);
    }
  }, this);

};

Motor.prototype.setPin = function(pin, value) {
  this.io.digitalWrite(pin, value);
};

Motor.prototype.setPWM = function(pin, value) {
  this.io.analogWrite(pin, value);
};

Motor.prototype.speed = function(opts) {
  var state = priv.get(this);

  if (typeof opts === "undefined") {
    return state.currentSpeed;
  } else {

    if (typeof opts === "number") {
      opts = {
        speed: opts
      };
    }

    opts.speed = Board.constrain(opts.speed, 0, 255);

    opts.saveSpeed = typeof opts.saveSpeed !== "undefined" ?
      opts.saveSpeed : true;

    if (opts.speed < this.threshold) {
      opts.speed = 0;
    }

    state.isOn = opts.speed === 0 ? false : true;

    if (opts.saveSpeed) {
      state.currentSpeed = opts.speed;
    }

    if (opts.braking) {
      state.braking = true;
    }

    if (this.invertPWM && this.direction.value === 1) {
      opts.speed ^= 0xff;
    }

    this.setPWM(this.pins.pwm, opts.speed);

    return this;
  }

};

// start a motor - essentially just switch it on like a normal motor
Motor.prototype.start = function(speed) {
  // Send a signal to turn on the motor and run at given speed in whatever
  // direction is currently set.
  if (this.pins.brake && this.braking) {
    this.setPin(this.pins.brake, 0);
  }

  // get current speed if nothing provided.
  speed = typeof speed !== "undefined" ?
    speed : this.speed();

  this.speed({
    speed: speed,
    braking: false
  });

  // "start" event is fired when the motor is started
  if (speed > 0) {
    process.nextTick(this.emit.bind(this, "start"));
  }

  return this;
};

Motor.prototype.stop = function() {
  this.speed({
    speed: 0,
    saveSpeed: false
  });
  process.nextTick(this.emit.bind(this, "stop"));

  return this;
};

Motor.prototype.brake = function(duration) {
  if (typeof this.pins.brake === "undefined") {
    if (this.board.io.name !== "Mock") {
      console.log("Non-braking motor type");
    }
    this.stop();
  } else {
    this.setPin(this.pins.brake, 1);
    this.setPin(this.pins.dir, 1);
    this.speed({
      speed: 255,
      saveSpeed: false,
      braking: true
    });
    process.nextTick(this.emit.bind(this, "brake"));

    if (duration) {
      var motor = this;
      this.board.wait(duration, function() {
        motor.resume();
      });
    }
  }

  return this;
};

Motor.prototype.release = function() {
  this.resume();
  process.nextTick(this.emit.bind(this, "release"));

  return this;
};

Motor.prototype.resume = function() {
  var speed = this.speed();
  this.dir(this.direction);
  this.start(speed);

  return this;
};

Motor.prototype.enable = function() {
  var state = priv.get(this);
  if (typeof this.pins.enable !== "undefined" && !this.enabled) {
    this.setPin(this.pins.enable, 1);
    state.enabled = true;
  }
};

Motor.prototype.disable = function() {
  var state = priv.get(this);
  if (typeof this.pins.enable !== "undefined" && this.enabled) {
    this.setPin(this.pins.enable, 0);
    state.enabled = false;
  }
};

[
  /**
   * forward Turn the Motor in its forward direction
   * fwd Turn the Motor in its forward direction
   *
   * @param  {Number} 0-255, 0 is stopped, 255 is fastest
   * @return {Object} this
   */
  {
    name: "forward",
    abbr: "fwd",
    value: 1
  },
  /**
   * reverse Turn the Motor in its reverse direction
   * rev Turn the Motor in its reverse direction
   *
   * @param  {Number} 0-255, 0 is stopped, 255 is fastest
   * @return {Object} this
   */
  {
    name: "reverse",
    abbr: "rev",
    value: 0
  }
].forEach(function(dir) {
  var method = function(speed) {
    this.dir(dir);
    this.start(speed);
    return this;
  };

  Motor.prototype[dir.name] = Motor.prototype[dir.abbr] = method;
});

Motor.SHIELD_CONFIGS = {
  ADAFRUIT_V1: {
    M1: {
      pins: {
        pwm: 11
      },
      register: {
        data: 8,
        clock: 4,
        latch: 12
      },
      bits: {
        a: 2,
        b: 3
      }
    },
    M2: {
      pins: {
        pwm: 3
      },
      register: {
        data: 8,
        clock: 4,
        latch: 12
      },
      bits: {
        a: 1,
        b: 4
      }
    },
    M3: {
      pins: {
        pwm: 6
      },
      register: {
        data: 8,
        clock: 4,
        latch: 12
      },
      bits: {
        a: 5,
        b: 7
      }
    },
    M4: {
      pins: {
        pwm: 5
      },
      register: {
        data: 8,
        clock: 4,
        latch: 12
      },
      bits: {
        a: 0,
        b: 6
      }
    }
  },
  ADAFRUIT_V2: {
    M1: {
      pins: {
        pwm: 8,
        dir: 9,
        cdir: 10
      },
      address: 0x60,
      controller: "PCA9685"
    },
    M2: {
      pins: {
        pwm: 13,
        dir: 12,
        cdir: 11
      },
      address: 0x60,
      controller: "PCA9685"
    },
    M3: {
      pins: {
        pwm: 2,
        dir: 3,
        cdir: 4
      },
      address: 0x60,
      controller: "PCA9685"
    },
    M4: {
      pins: {
        pwm: 7,
        dir: 6,
        cdir: 5
      },
      address: 0x60,
      controller: "PCA9685"
    }
  },
  SEEED_STUDIO: {
    A: {
      pins: {
        pwm: 9,
        dir: 8,
        cdir: 11
      }
    },
    B: {
      pins: {
        pwm: 10,
        dir: 12,
        cdir: 13
      }
    }
  },
  FREETRONICS_HBRIDGE: {
    A: {
      pins: {
        pwm: 6,
        dir: 4,
        cdir: 7
      }
    },
    B: {
      pins: {
        pwm: 5,
        dir: 3,
        cdir: 2
      }
    }
  },
  ARDUINO_MOTOR_SHIELD_R3_1: {
    A: {
      pins: {
        pwm: 3,
        dir: 12
      }
    },
    B: {
      pins: {
        pwm: 11,
        dir: 13
      }
    }
  },
  ARDUINO_MOTOR_SHIELD_R3_2: {
    A: {
      pins: {
        pwm: 3,
        dir: 12,
        brake: 9
      }
    },
    B: {
      pins: {
        pwm: 11,
        dir: 13,
        brake: 8
      }
    }
  },
  ARDUINO_MOTOR_SHIELD_R3_3: {
    A: {
      pins: {
        pwm: 3,
        dir: 12,
        brake: 9,
        current: "A0"
      }
    },
    B: {
      pins: {
        pwm: 11,
        dir: 13,
        brake: 8,
        current: "A1"
      }
    }
  },
  DF_ROBOT: {
    A: {
      pins: {
        pwm: 6,
        dir: 7
      }
    },
    B: {
      pins: {
        pwm: 5,
        dir: 4
      }
    }
  },
  NKC_ELECTRONICS_KIT: {
    A: {
      pins: {
        pwm: 9,
        dir: 12
      }
    },
    B: {
      pins: {
        pwm: 10,
        dir: 13
      }
    }
  },
  RUGGED_CIRCUITS: {
    A: {
      pins: {
        pwm: 3,
        dir: 12
      }
    },
    B: {
      pins: {
        pwm: 11,
        dir: 13
      }
    }
  },
  SPARKFUN_ARDUMOTO: {
    A: {
      pins: {
        pwm: 3,
        dir: 12
      }
    },
    B: {
      pins: {
        pwm: 11,
        dir: 13
      }
    }
  },
  POLOLU_DRV8835_SHIELD: {
    M1: {
      pins: {
        pwm: 9,
        dir: 7
      }
    },
    M2: {
      pins: {
        pwm: 10,
        dir: 8
      }
    }
  },
  MICRO_MAGICIAN_V2: {
    A: {
      pins: {
        pwm: 6,
        dir: 8
      },
      invertPWM: true
    },
    B: {
      pins: {
        pwm: 5,
        dir: 7
      },
      invertPWM: true
    }
  },
  SPARKFUN_LUDUS: {
    A: {
      pins: {
        pwm: 3,
        dir: 4,
        cdir: 5
      }
    },
    B: {
      pins: {
        pwm: 6,
        dir: 7,
        cdir: 8
      }
    }
  },
  SPARKFUN_DUAL_HBRIDGE_EDISON_BLOCK: {
    A: {
      pins: {
        pwm: 20,
        dir: 33,
        cdir: 46,
        enable: 47
      }
    },
    B: {
      pins: {
        pwm: 14,
        dir: 48,
        cdir: 36,
        enable: 47
      }
    }
  },

};


/**
 * Motors()
 * new Motors()
 */
function Motors(numsOrObjects) {
  if (!(this instanceof Motors)) {
    return new Motors(numsOrObjects);
  }

  Object.defineProperty(this, "type", {
    value: Motor
  });

  Collection.call(this, numsOrObjects);
}

Motors.prototype = Object.create(Collection.prototype, {
  constructor: {
    value: Motors
  }
});


/*
 * Motors, forward(speed)/fwd(speed)
 *
 * eg. array.forward(speed);

 * Motors, reverse(speed)/rev(speed)
 *
 * eg. array.reverse(speed);

 * Motors, start(speed)
 *
 * eg. array.start(speed);

 * Motors, stop()
 *
 * eg. array.stop();

 * Motors, brake()
 *
 * eg. array.brake();

 * Motors, release()
 *
 * eg. array.release();
 */

Collection.installMethodForwarding(
  Motors.prototype, Motor.prototype
);


// Assign Motors Collection class as static "method" of Motor.
// TODO: Eliminate .Array for 1.0.0
Motor.Array = Motors;
Motor.Collection = Motors;

/* istanbul ignore else */
if (IS_TEST_MODE) {
  Motor.purge = function() {
    priv.clear();
    registers.clear();
  };
}

module.exports = Motor;
