import * as Blockly from 'blockly';

// C++ operator precedence (lower number = tighter binding)
export const Order = {
  ATOMIC: 0,
  MEMBER: 1,
  FUNCTION_CALL: 2,
  POSTFIX: 3,
  UNARY_PREFIX: 4,
  MULTIPLICATIVE: 5,
  ADDITIVE: 6,
  SHIFT: 7,
  RELATIONAL: 8,
  EQUALITY: 9,
  BITWISE_AND: 10,
  BITWISE_XOR: 11,
  BITWISE_OR: 12,
  LOGICAL_AND: 13,
  LOGICAL_OR: 14,
  CONDITIONAL: 15,
  ASSIGNMENT: 16,
  COMMA: 17,
  NONE: 99,
};

class ArduinoGenerator extends Blockly.CodeGenerator {
  constructor() {
    super('Arduino');
    this.INDENT = '  ';

    this.addReservedWords(
      'setup,loop,void,int,long,float,double,char,bool,byte,unsigned,' +
      'String,HIGH,LOW,INPUT,OUTPUT,INPUT_PULLUP,LED_BUILTIN,true,false,' +
      'Serial,Wire,SPI,Servo,pinMode,digitalWrite,digitalRead,' +
      'analogWrite,analogRead,delay,delayMicroseconds,millis,micros,' +
      'tone,noTone,pulseIn,map,constrain,min,max,abs,random,randomSeed,' +
      'sizeof,return,if,else,for,while,do,switch,case,break,continue,' +
      'define,include,class,struct,enum,typedef,static,const,volatile'
    );

    this.ORDER_OVERRIDES = [
      [Order.FUNCTION_CALL, Order.MEMBER],
      [Order.MEMBER, Order.FUNCTION_CALL],
    ];
  }

  init(workspace) {
    super.init(workspace);
    this.definitions_ = Object.create(null);
    this.setupCode_ = [];
    this.functionDefinitions_ = Object.create(null);
    if (!this.nameDB_) {
      this.nameDB_ = new Blockly.Names(this.RESERVED_WORDS_);
    } else {
      this.nameDB_.reset();
    }
    this.nameDB_.setVariableMap(workspace.getVariableMap());
    this.nameDB_.populateVariables(workspace);
    this.nameDB_.populateProcedures(workspace);
    this.isInitialized = true;
  }

  finish(code) {
    const includes = [];
    const globals = [];
    for (const [key, val] of Object.entries(this.definitions_)) {
      if (val.startsWith('#include')) {
        includes.push(val);
      } else {
        globals.push(val);
      }
    }

    const funcDefs = Object.values(this.functionDefinitions_);

    const setupBody = this.setupCode_.map(s => this.INDENT + s).join('\n');
    const indentedCode = code ? this.prefixLines(code, this.INDENT) : '';

    const parts = [];
    if (includes.length) parts.push(includes.join('\n'));
    if (globals.length) parts.push(globals.join('\n'));
    if (funcDefs.length) parts.push(funcDefs.join('\n\n'));

    const setupContent = [setupBody, indentedCode].filter(Boolean).join('\n');
    parts.push(`void setup() {\n${setupContent}\n}`);
    parts.push('void loop() {\n}');

    super.finish('');
    this.isInitialized = false;
    return parts.join('\n\n') + '\n';
  }

  scrubNakedValue(line) {
    return line + ';\n';
  }

  quote_(str) {
    str = str.replace(/\\/g, '\\\\')
             .replace(/\n/g, '\\n')
             .replace(/"/g, '\\"');
    return '"' + str + '"';
  }

  scrub_(block, code, opt_thisOnly) {
    let commentCode = '';
    if (!block.outputConnection || !block.outputConnection.targetBlock()) {
      let comment = block.getCommentText();
      if (comment) {
        comment = Blockly.utils.string.wrap(comment, this.COMMENT_WRAP - 3);
        commentCode += this.prefixLines(comment + '\n', '// ');
      }
      for (let i = 0; i < block.inputList.length; i++) {
        const input = block.inputList[i];
        if (input.connection && input.connection.type === 1) {
          const childBlock = input.connection.targetBlock();
          if (childBlock) {
            comment = this.allNestedComments(childBlock);
            if (comment) {
              commentCode += this.prefixLines(comment, '// ');
            }
          }
        }
      }
    }
    const nextBlock = block.nextConnection && block.nextConnection.targetBlock();
    const nextCode = opt_thisOnly ? '' : this.blockToCode(nextBlock);
    return commentCode + code + nextCode;
  }

  getVariableName(nameOrId) {
    return this.nameDB_.getName(nameOrId, Blockly.Names.NameType.VARIABLE);
  }

  addSetupCode(code) {
    if (this.setupCode_.indexOf(code) === -1) {
      this.setupCode_.push(code);
    }
  }

  addInclude(key, code) {
    this.definitions_[key] = code;
  }

  addDeclaration(key, code) {
    this.definitions_[key] = code;
  }
}

export const arduinoGenerator = new ArduinoGenerator();
