/*
 * Copyright 2000-2016 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jetbrains.java.decompiler.code;

import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.List;

import org.jetbrains.java.decompiler.code.interpreter.Util;
import org.jetbrains.java.decompiler.main.DecompilerContext;
import org.jetbrains.java.decompiler.struct.StructContext;
import org.jetbrains.java.decompiler.util.TextUtil;
import org.jetbrains.java.decompiler.util.VBStyleCollection;

public abstract class InstructionSequence {

  // *****************************************************************************
  // private fields
  // *****************************************************************************

  protected final VBStyleCollection<Instruction, Integer> collinstr;

  protected int pointer = 0;

  protected ExceptionTable exceptionTable = ExceptionTable.EMPTY;

  protected InstructionSequence() {
    this(new VBStyleCollection<>());
  }

  protected InstructionSequence(VBStyleCollection<Instruction, Integer> collinstr) {
    this.collinstr = collinstr;
  }

  // *****************************************************************************
  // public methods
  // *****************************************************************************

  // to nbe overwritten
  public InstructionSequence clone() {
    return null;
  }

  public void clear() {
    collinstr.clear();
    pointer = 0;
    exceptionTable = ExceptionTable.EMPTY;
  }

  public void addInstruction(Instruction inst, int offset) {
    collinstr.addWithKey(inst, offset);
  }

  public void addInstruction(int index, Instruction inst, int offset) {
    collinstr.addWithKeyAndIndex(index, inst, offset);
  }

  public void addSequence(InstructionSequence seq) {
    for (int i = 0; i < seq.length(); i++) {
      addInstruction(seq.getInstr(i), -1); // TODO: any sensible value possible?
    }
  }

  public void removeInstruction(int index) {
    collinstr.remove(index);
  }

  public void removeLast() {
    if (!collinstr.isEmpty()) {
      collinstr.remove(collinstr.size() - 1);
    }
  }

  public Instruction getCurrentInstr() {
    return collinstr.get(pointer);
  }

  public Instruction getInstr(int index) {
    return collinstr.get(index);
  }

  public Instruction getLastInstr() {
    return collinstr.getLast();
  }

  public int getCurrentOffset() {
    return collinstr.getKey(pointer).intValue();
  }

  public int getOffset(int index) {
    return collinstr.getKey(index).intValue();
  }

  public int getPointerByAbsOffset(int offset) {
    Integer absoffset = new Integer(offset);
    if (collinstr.containsKey(absoffset)) {
      return collinstr.getIndexByKey(absoffset);
    } else {
      return -1;
    }
  }

  public int getPointerByRelOffset(int offset) {
    Integer absoffset = new Integer(collinstr.getKey(pointer).intValue() + offset);
    if (collinstr.containsKey(absoffset)) {
      return collinstr.getIndexByKey(absoffset);
    } else {
      return -1;
    }
  }

  public void setPointerByAbsOffset(int offset) {
    Integer absoffset = new Integer(collinstr.getKey(pointer).intValue() + offset);
    if (collinstr.containsKey(absoffset)) {
      pointer = collinstr.getIndexByKey(absoffset);
    }
  }

  public int length() {
    return collinstr.size();
  }

  public boolean isEmpty() {
    return collinstr.isEmpty();
  }

  public void addToPointer(int diff) {
    this.pointer += diff;
  }

  public String toString() {
    return toString(0);
  }

  public String toString(int indent) {

    String new_line_separator = DecompilerContext.getNewLineSeparator();

    StringBuilder buf = new StringBuilder();

    for (int i = 0; i < collinstr.size(); i++) {
      buf.append(TextUtil.getIndentString(indent));
      buf.append(collinstr.getKey(i).intValue());
      buf.append(": ");
      buf.append(collinstr.get(i).toString());
      buf.append(new_line_separator);
    }

    return buf.toString();
  }

  public void writeCodeToStream(DataOutputStream out) throws IOException {

    for (int i = 0; i < collinstr.size(); i++) {
      collinstr.get(i).writeToStream(out, collinstr.getKey(i).intValue());
    }
  }

  public void writeExceptionsToStream(DataOutputStream out) throws IOException {

    List<ExceptionHandler> handlers = exceptionTable.getHandlers();

    out.writeShort(handlers.size());
    for (int i = 0; i < handlers.size(); i++) {
      handlers.get(i).writeToStream(out);
    }
  }

  public void sortHandlers(final StructContext context) {

    Collections.sort(exceptionTable.getHandlers(), (handler0, handler1) -> {

      if (handler0.to == handler1.to) {
        if (handler0.exceptionClass == null) {
          return 1;
        } else {
          if (handler1.exceptionClass == null) {
            return -1;
          } else if (handler0.exceptionClass.equals(handler1.exceptionClass)) {
            return (handler0.from > handler1.from) ? -1 : 1; // invalid code
          } else {
            if (Util.instanceOf(context, handler0.exceptionClass, handler1.exceptionClass)) {
              return -1;
            } else {
              return 1;
            }
          }
        }
      } else {
        return (handler0.to > handler1.to) ? 1 : -1;
      }
    });
  }

  // *****************************************************************************
  // getter and setter methods
  // *****************************************************************************

  public int getPointer() {
    return pointer;
  }

  public void setPointer(int pointer) {
    this.pointer = pointer;
  }

  public ExceptionTable getExceptionTable() {
    return exceptionTable;
  }

  public void setExceptionTable(ExceptionTable exceptionTable) {
    this.exceptionTable = exceptionTable;
  }
}
