package com.bytezone.diskbrowser.applefile;

import java.util.ArrayList;
import java.util.List;

import com.bytezone.diskbrowser.applefile.PascalCodeStatement.Jump;
import com.bytezone.diskbrowser.utilities.HexFormatter;

public class PascalProcedure
{
  // all procedures have these fields
  byte[] buffer;
  int procOffset;
  int offset;
  int slot;
  boolean valid;

  // only valid procedures have these fields
  int procedureNo;
  int procLevel;
  int codeStart;
  int codeEnd;
  int parmSize;
  int dataSize;
  List<PascalCodeStatement> statements = new ArrayList<PascalCodeStatement> ();
  AssemblerProgram assembler;
  int jumpTable = -8;

  public PascalProcedure (byte[] buffer, int slot)
  {
    this.buffer = buffer;
    this.slot = slot;
    int p = buffer.length - 2 - slot * 2;
    offset = HexFormatter.intValue (buffer[p], buffer[p + 1]);
    procOffset = p - offset;
    valid = procOffset > 0;

    if (valid)
    {
      procedureNo = buffer[procOffset] & 0xFF;
      procLevel = buffer[procOffset + 1] & 0xFF;
      codeStart = HexFormatter.intValue (buffer[procOffset - 2], buffer[procOffset - 1]);
      codeEnd = HexFormatter.intValue (buffer[procOffset - 4], buffer[procOffset - 3]);
      parmSize = HexFormatter.intValue (buffer[procOffset - 6], buffer[procOffset - 5]);
      dataSize = HexFormatter.intValue (buffer[procOffset - 8], buffer[procOffset - 7]);
    }
  }

  private void decode ()
  {
    if (statements.size () > 0 || assembler != null)
      return;
    int ptr = procOffset - codeStart - 2;
    int max = procOffset + jumpTable;

    if (codeEnd == 0)
    {
      int len = codeStart + jumpTable + 2;
      if (len > 0)
      {
        byte[] asmBuf = new byte[len];
        System.arraycopy (buffer, ptr, asmBuf, 0, len);
        assembler = new AssemblerProgram ("Proc", asmBuf, ptr);
      }
      return;
    }

    while (ptr < max)
    {
      //      System.out.printf ("ptr:%d, max:%d, buf:%d %n", ptr, max, buffer.length);
      if (ptr >= buffer.length || ptr < 0)
      {
        System.out.printf ("Ptr outside buffer: %d %d%n", ptr, buffer.length);
        break;
      }
      PascalCodeStatement cs = new PascalCodeStatement (buffer, ptr, procOffset);
      if (cs.length <= 0)
      {
        System.out.println ("error - length <= 0 : " + cs);
        break;
      }
      statements.add (cs);
      if (cs.val == 185 || cs.val == 161)
        if (cs.p1 < jumpTable)
        {
          jumpTable = cs.p1;
          max = procOffset + jumpTable;
        }
      ptr += cs.length;
    }

    // Tidy up left-over bytes at the end
    if (statements.size () > 1)
    {
      PascalCodeStatement lastStatement = statements.get (statements.size () - 1);
      PascalCodeStatement secondLastStatement = statements.get (statements.size () - 2);
      if (lastStatement.val == 0 && (secondLastStatement.val == 0xD6
          || secondLastStatement.val == 0xC1 || secondLastStatement.val == 0xAD))
        statements.remove (statements.size () - 1);
    }

    // Mark statements that are jump targets
    int actualEnd = procOffset - codeEnd - 4;
    for (PascalCodeStatement cs : statements)
    {
      if (cs.ptr == actualEnd)
      {
        cs.jumpTarget = true;
        continue;
      }
      for (Jump cj : cs.jumps)
        for (PascalCodeStatement cs2 : statements)
          if (cs2.ptr == cj.addressTo)
          {
            cs2.jumpTarget = true;
            break;
          }
    }
  }

  public List<PascalCodeStatement> extractStrings ()
  {
    decode ();
    List<PascalCodeStatement> strings = new ArrayList<PascalCodeStatement> ();
    for (PascalCodeStatement cs : statements)
      if (cs.val == 166)
        strings.add (cs);
    return strings;
  }

  @Override
  public String toString ()
  {
    if (!valid)
      return "";
    decode ();

    StringBuilder text = new StringBuilder ("\nProcedure Header\n================\n\n");

    if (false)
      text.append (
          HexFormatter.format (buffer, procOffset + jumpTable, 2 - jumpTable) + "\n\n");

    text.append (
        String.format ("Level.......%5d     %02X%n", procLevel, procLevel & 0xFF));
    text.append (String.format ("Proc no.....%5d     %02X%n", procedureNo, procedureNo));
    text.append (String.format ("Code entry..%5d   %04X  (%04X - %04X = %04X)%n",
        codeStart, codeStart, (procOffset - 2), codeStart, (procOffset - codeStart - 2)));
    text.append (String.format ("Code exit...%5d   %04X", codeEnd, codeEnd));
    if (codeEnd > 0)
      text.append (String.format ("  (%04X - %04X = %04X)%n", (procOffset - 4), codeEnd,
          (procOffset - codeEnd - 4)));
    else
      text.append (String.format ("%n"));
    text.append (String.format ("Parm size...%5d   %04X%n", parmSize, parmSize));
    text.append (String.format ("Data size...%5d   %04X%n%n", dataSize, dataSize));

    text.append ("Procedure Code\n==============\n\n");

    int ptr = procOffset - codeStart - 2;
    if (false)
      text.append (HexFormatter.format (buffer, ptr, codeStart + jumpTable + 2) + "\n\n");

    if (codeEnd == 0)
    {
      if (assembler != null)
        text.append (assembler.getAssembler () + "\n");
      else
        text.append ("Null assembler in PascalProcedure");
    }
    else
    {
      for (PascalCodeStatement cs : statements)
        text.append (cs);

      if (jumpTable < -8 && false)
      {
        text.append ("\nJump table:\n");
        for (int i = procOffset + jumpTable; i < procOffset - 8; i += 2)
        {
          ptr = i - ((buffer[i + 1] & 0xFF) * 256 + (buffer[i] & 0xFF));
          text.append (String.format ("%05X : %02X %02X  --> %04X%n", i, buffer[i],
              buffer[i + 1], ptr));
        }
      }
    }
    return text.toString ();
  }
}