/*
Copyright 2011-2016 Google Inc. All Rights Reserved.

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 com.google.security.zynamics.zylib.yfileswrap.gui.zygraph.realizers;

import com.google.common.base.Preconditions;
import com.google.security.zynamics.binnavi.CUtilityFunctions;
import com.google.security.zynamics.zylib.general.ListenerProvider;
import com.google.security.zynamics.zylib.gui.zygraph.nodes.ZyNodeData;
import com.google.security.zynamics.zylib.gui.zygraph.realizers.IRealizerUpdater;
import com.google.security.zynamics.zylib.gui.zygraph.realizers.IZyNodeRealizerListener;
import com.google.security.zynamics.zylib.gui.zygraph.realizers.ZyLabelContent;
import com.google.security.zynamics.zylib.yfileswrap.gui.zygraph.nodes.ZyGraphNode;

import y.base.Edge;
import y.base.Node;
import y.geom.YPoint;
import y.view.EdgeRealizer;
import y.view.Graph2D;
import y.view.LineType;
import y.view.Port;
import y.view.ShapeNodeRealizer;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;

/**
 * Realizer that is used to display graph nodes.
 */
public abstract class ZyNodeRealizer<NodeType extends ZyGraphNode<?>> extends ShapeNodeRealizer
implements IZyNodeRealizer {
  /**
   * User-specific data that is associated with the realizer.
   */
  private ZyNodeData<?> m_userData;

  /**
   * Node updater class that is used to update the content of the realizer.
   */
  private IRealizerUpdater<?> m_updater;

  /**
   * Listeners that are notified about changes in the node realizer.
   */
  private final ListenerProvider<IZyNodeRealizerListener<?>> m_listeners = new ListenerProvider<>();

  protected boolean m_isHighLighted = false;

  private void notifyLocationChanged(final double x, final double y) {
    for (final IZyNodeRealizerListener<?> listener : m_listeners) {
      try {
        listener.changedLocation(this, x, y);
      } catch (final Exception exception) {
        CUtilityFunctions.logException(exception);
      }
    }
  }

  protected void notifyHasRegenerated() {
    for (final IZyNodeRealizerListener<?> listener : m_listeners) {
      try {
        listener.regenerated(this);
      } catch (final Exception exception) {
        CUtilityFunctions.logException(exception);
      }
    }
  }

  protected void scalePortCoordinates(
      final Node node, final double wOld, final double wNew, final double hOld, final double hNew) {
    final Graph2D graph = (Graph2D) node.getGraph();
    final double wScaling = wOld > 0 ? wNew / wOld : 1.0d;
    final double hScaling = hOld > 0 ? hNew / hOld : 1.0d;

    for (Edge e = node.firstOutEdge(); e != null; e = e.nextOutEdge()) {
      final EdgeRealizer er = graph.getRealizer(e);
      final Port port = er.getSourcePort();
      final double px = port.getOffsetX() * wScaling;
      final double py = port.getOffsetY() * hScaling;
      port.setOffsets(px, py);
      graph.setSourcePointRel(e, new YPoint(px, py));
    }

    for (Edge e = node.firstInEdge(); e != null; e = e.nextInEdge()) {
      final EdgeRealizer er = graph.getRealizer(e);
      final Port port = er.getTargetPort();
      final double px = port.getOffsetX() * wScaling;
      final double py = port.getOffsetY() * hScaling;
      port.setOffsets(px, py);
      graph.setTargetPointRel(e, new YPoint(px, py));
    }
  }

  /**
   * Adds a listener that is notified about changes in the node realizer.
   *
   * @param listener The listener object to add.
   */
  @Override
  public void addListener(final IZyNodeRealizerListener<?> listener) {
    m_listeners.addListener(listener);
  }

  /**
   * Returns the node content that is displayed by the realizer.
   *
   * @return The node content that is displayed by the realizer.
   */
  @Override
  public abstract ZyLabelContent getNodeContent();

  @Override
  public IRealizerUpdater<?> getUpdater() {
    return m_updater;
  }

  /**
   * Returns the user data associated with the realizer.
   *
   * @return The user data associated with the realizer.
   */
  @Override
  public ZyNodeData<?> getUserData() {
    return m_userData;
  }

  @Override
  public void moveBy(final double x, final double y) {
    super.moveBy(x, y);

    notifyLocationChanged(getX(), getY());
  }

  @Override
  public void paintSloppy(final Graphics2D gfx) {
    if (isSelected() || m_isHighLighted) {
      final LineType old = getLineType();
      setLineType(LineType.LINE_5);
      paintFilledShape(gfx);
      paintShapeBorder(gfx);
      setLineType(old);
    } else {
      paintFilledShape(gfx);
      paintShapeBorder(gfx);
    }
  }

  /**
   * Converts a y coordinate into a line number of the node content.
   *
   * @param y The y coordinate.
   * @return The number of the line shown at the y address or -1 if there is no line at the given
   *         coordinates.
   */
  @Override
  public int positionToRow(final double y) {
    // TODO: This does not really work because line heights are not constant.

    final ZyLabelContent content = getNodeContent();

    final Rectangle2D contentBounds = getNodeContent().getBounds();
    final double yratio = getHeight() / contentBounds.getHeight();

    final int row = (int) ((y / yratio - content.getPaddingTop()) / content.getLineHeight());

    return row >= content.getLineCount() ? -1 : row;
  }

  /**
   * Regenerates the content of the realizer.
   */
  @Override
  public void regenerate() {
    final ZyLabelContent content = getNodeContent();

    final double widthOld = content.getBounds().getWidth();
    final double heightOld = content.getBounds().getHeight();

    if (m_updater != null) {
      m_updater.generateContent(this, content);
    }

    final Rectangle2D bounds = content.getBounds();

    setSize(bounds.getWidth(), bounds.getHeight());

    scalePortCoordinates(getNode(), widthOld, bounds.getWidth(), heightOld, bounds.getHeight());

    notifyHasRegenerated();

    // TODO(jannewger): check if this method is redundant - it seems that client code often calls
    // regeneerate followe by a redraw operation. That would mean that we needlessly redraw two
    // times.
    repaint();
  }

  /**
   * Removes a listener object from the realizer.
   *
   * @param listener The listener object to be removed.
   */
  @Override
  public void removeListener(final IZyNodeRealizerListener<?> listener) {
    m_listeners.removeListener(listener);
  }

  @Override
  public double rowToPosition(final int row) {
    final ZyLabelContent content = getNodeContent();

    return content.getPaddingTop() + row * content.getLineHeight();
  }

  @Override
  public void setCenter(final double x, final double y) {
    super.setCenter(x, y);

    notifyLocationChanged(getX(), getY());
  }

  @Override
  public void setFillColor(final Color color) {
    if (super.getFillColor() != color) {
      super.setFillColor(color);
      updateContentSelectionColor();
    }
  }

  @Override
  public void setLineType(final LineType linetype) {
    m_isHighLighted = linetype == LineType.LINE_5 || linetype == LineType.DASHED_5
        || linetype == LineType.DOTTED_5 || linetype == LineType.DASHED_DOTTED_5;

    super.setLineType(linetype);
  }

  @Override
  public void setLocation(final double x, final double y) {
    super.setLocation(x, y);
    notifyLocationChanged(x, y);
  }

  @Override
  public void setSelected(final boolean selected) {
    if (super.isSelected() != selected) {
      super.setSelected(selected);

      updateContentSelectionColor();

      for (final IZyNodeRealizerListener<?> listener : m_listeners) {
        try {
          listener.changedSelection(this);
        } catch (final Exception exception) {
          CUtilityFunctions.logException(exception);
        }
      }
    }
  }

  @Override
  public void setSize(final double x, final double y) {
    if (super.getX() != x || super.getY() != y) {
      super.setSize(x, y);

      for (final IZyNodeRealizerListener<?> listener : m_listeners) {
        try {
          listener.changedSize(this, x, y);
        } catch (final Exception exception) {
          CUtilityFunctions.logException(exception);
        }
      }
    }
  }

  @Override
  public void setUpdater(final IRealizerUpdater<? extends ZyGraphNode<?>> updater) {
    m_updater = updater;

    if (updater != null) {
      updater.setRealizer(this);
    }
  }

  /**
   * Sets the user data associated with the realizer.
   *
   * @param data The new user data object.
   */
  @Override
  public void setUserData(final ZyNodeData<?> data) {
    Preconditions.checkNotNull(data);

    m_userData = data;
  }

  @Override
  public void setVisible(final boolean visible) {
    if (super.isVisible() != visible) {
      super.setVisible(visible);

      updateContentSelectionColor();

      for (final IZyNodeRealizerListener<?> listener : m_listeners) {
        try {
          listener.changedVisibility(this);
        } catch (final Exception exception) {
          CUtilityFunctions.logException(exception);
        }
      }
    }
  }

  @Override
  public void updateContentSelectionColor() {
    // TODO: This is kind of a hack, should be improved
    final ZyLabelContent content = getNodeContent();

    if (content.isSelectable()) {
      content.updateContentSelectionColor(getFillColor(), isSelected());
    }
  }
}
