// Copyright (c) 2009 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.sdk.internal;

import java.util.AbstractList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.chromium.sdk.Script;
import org.chromium.sdk.UpdatableScript;
import org.chromium.sdk.internal.liveeditprotocol.LiveEditResult;

/**
 * An objects that holds data for a "script" which is a part of a resource
 * loaded into the browser, identified by its original document URL, line offset
 * in the original document, and the line count this script spans.
 */
public abstract class ScriptBase<ID> implements Script {
  /**
   * An object containing data that uniquely identify a V8 script chunk.
   */
  public static class Descriptor<ID> {
    public final Type type;

    public final String name;

    public final int lineOffset;

    public final int columnOffset;

    public final int endLine;

    public final ID id;

    public Descriptor(Type type, ID id, String name, int lineOffset, int columnOffset,
        int lineCount) {
      this.type = type;
      this.id = id;
      this.name = name;
      this.lineOffset = lineOffset;
      this.columnOffset = columnOffset;
      this.endLine = lineOffset + lineCount - 1;
    }

    @Override
    public int hashCode() {
      return
          name != null ? name.hashCode() : id.hashCode() * 0x101 +
          lineOffset * 0x1001 + columnOffset * 0x10001 +
          endLine * 0x100001;
    }

    @Override
    public boolean equals(Object obj) {
      if (obj == this) {
        return true;
      }
      if (!(obj instanceof Descriptor)) {
        return false;
      }
      Descriptor<?> that = (Descriptor<?>) obj;
      // The id equality is stronger than the name equality.
      return this.id.equals(that.id) &&
          this.lineOffset == that.lineOffset &&
          this.columnOffset == that.columnOffset &&
          this.endLine == that.endLine;
    }
  }

  private final Descriptor<ID> descriptor;

  private volatile String source = null;

  private volatile boolean isCollected = false;

  /**
   * @param descriptor of the script retrieved from a "scripts" response
   */
  public ScriptBase(Descriptor<ID> descriptor) {
    this.descriptor = descriptor;
    this.source = null;
  }

  @Override
  public Type getType() {
    return this.descriptor.type;
  }

  @Override
  public String getName() {
    return descriptor.name;
  }

  @Override
  public int getStartLine() {
    return descriptor.lineOffset;
  }

  @Override
  public int getStartColumn() {
    return descriptor.columnOffset;
  }

  @Override
  public int getEndLine() {
    return descriptor.endLine;
  }

  @Override
  public ID getId() {
    return descriptor.id;
  }

  @Override
  public boolean isCollected() {
    return isCollected;
  }

  @Override
  public String getSource() {
    return source;
  }

  @Override
  public boolean hasSource() {
    return source != null;
  }

  public void setSource(String source) {
    this.source = source;
  }

  public void setCollected() {
    isCollected = true;
  }

  protected static class UpdateResultParser {
    public static UpdatableScript.ChangeDescription wrapChangeDescription(
        final LiveEditResult previewDescription) {
      if (previewDescription == null) {
        return null;
      }
      return new UpdatableScript.ChangeDescription() {
        @Override public UpdatableScript.OldFunctionNode getChangeTree() {
          return OLD_WRAPPER.wrap(previewDescription.change_tree());
        }

        @Override public String getCreatedScriptName() {
          return previewDescription.created_script_name();
        }

        @Override public boolean isStackModified() {
          return previewDescription.stack_modified();
        }

        @Override public TextualDiff getTextualDiff() {
          final LiveEditResult.TextualDiff protocolTextualData = previewDescription.textual_diff();
          if (protocolTextualData == null) {
            return null;
          }
          return new TextualDiff() {
            @Override public List<Long> getChunks() {
              return protocolTextualData.chunks();
            }
          };
        }
      };
    }

    private static class OldFunctionNodeImpl implements UpdatableScript.OldFunctionNode {
      private final LiveEditResult.OldTreeNode treeNode;
      private final FunctionPositions positions;
      private final FunctionPositions newPositions;

      OldFunctionNodeImpl(LiveEditResult.OldTreeNode treeNode) {
        this.treeNode = treeNode;
        this.positions = wrapPositions(treeNode.positions());
        if (treeNode.new_positions() == null) {
          this.newPositions = null;
        } else {
          this.newPositions = wrapPositions(treeNode.new_positions());
        }
      }
      @Override public String getName() {
        return treeNode.name();
      }
      @Override public ChangeStatus getStatus() {
        return statusCodes.get(treeNode.status());
      }
      @Override public String getStatusExplanation() {
        return treeNode.status_explanation();
      }
      @Override public List<? extends OldFunctionNode> children() {
        return wrapList(treeNode.children(), OLD_WRAPPER);
      }
      @Override public List<? extends NewFunctionNode> newChildren() {
        return wrapList(treeNode.new_children(), NEW_WRAPPER);
      }
      @Override public FunctionPositions getPositions() {
        return positions;
      }
      @Override public FunctionPositions getNewPositions() {
        return newPositions;
      }
      @Override public OldFunctionNode asOldFunction() {
        return this;
      }
    }
    private static class NewFunctionNodeImpl implements UpdatableScript.NewFunctionNode {
      private final LiveEditResult.NewTreeNode treeNode;
      private final FunctionPositions positions;
      NewFunctionNodeImpl(LiveEditResult.NewTreeNode treeNode) {
        this.treeNode = treeNode;
        this.positions = wrapPositions(treeNode.positions());
      }
      @Override public String getName() {
        return treeNode.name();
      }
      @Override public FunctionPositions getPositions() {
        return positions;
      }
      @Override public List<? extends NewFunctionNode> children() {
        return wrapList(treeNode.children(), NEW_WRAPPER);
      }
      @Override public OldFunctionNode asOldFunction() {
        return null;
      }
    }

    private static final Wrapper<LiveEditResult.OldTreeNode, OldFunctionNode> OLD_WRAPPER =
        new Wrapper<LiveEditResult.OldTreeNode, OldFunctionNode>() {
          @Override
          OldFunctionNode wrap(LiveEditResult.OldTreeNode original) {
            return new OldFunctionNodeImpl(original);
          }
    };

    private static final Wrapper<LiveEditResult.NewTreeNode, NewFunctionNode> NEW_WRAPPER =
        new Wrapper<LiveEditResult.NewTreeNode, NewFunctionNode>() {
          @Override
          NewFunctionNode wrap(LiveEditResult.NewTreeNode original) {
            return new NewFunctionNodeImpl(original);
          }
    };

    private static UpdatableScript.FunctionPositions wrapPositions(
        final LiveEditResult.Positions rawPositions) {
      return new UpdatableScript.FunctionPositions() {
        @Override public long getStart() {
          return rawPositions.start_position();
        }
        @Override public long getEnd() {
          return rawPositions.end_position();
        }
      };
    }

    private static abstract class Wrapper<FROM, TO> {
      abstract TO wrap(FROM original);
    }

    private static <FROM, TO> List<TO> wrapList(final List<? extends FROM> originalList,
        final Wrapper<FROM, TO> wrapper) {
      return new AbstractList<TO>() {
        @Override public TO get(int index) {
          return wrapper.wrap(originalList.get(index));
        }
        @Override public int size() {
          return originalList.size();
        }
      };
    }

    private static final Map<String, ChangeStatus> statusCodes;
    static {
      statusCodes = new HashMap<String, ChangeStatus>(5);
      statusCodes.put("unchanged", ChangeStatus.UNCHANGED);
      statusCodes.put("source changed", ChangeStatus.NESTED_CHANGED);
      statusCodes.put("changed", ChangeStatus.CODE_PATCHED);
      statusCodes.put("damaged", ChangeStatus.DAMAGED);
    }
  }

  @Override
  public int hashCode() {
    return
        descriptor.hashCode() * 0x101 +
        (hasSource() ? (source.hashCode() * 0x1001) : 0);
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }
    if (!(obj instanceof ScriptBase)) {
      return false;
    }
    ScriptBase<?> that = (ScriptBase<?>) obj;
    return this.descriptor.equals(that.descriptor) && eq(this.source, that.source);
  }

  private static boolean eq(Object left, Object right) {
    return left == right || (left != null && left.equals(right));
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append("[Script (").append(hasSource()
        ? "has"
        : "no").append(" source): name=").append(getName()).append(", lineRange=[").append(
        getStartLine()).append(';').append(getEndLine()).append("]]");
    return sb.toString();
  }
}
