/*
 * Licensed Materials - Property of Rogue Wave Software, Inc. 
 * © Copyright Rogue Wave Software, Inc. 2014, 2016 
 * © Copyright IBM Corp. 2009, 2014
 * © Copyright ILOG 1996, 2009
 * All Rights Reserved.
 *
 * Note to U.S. Government Users Restricted Rights:
 * The Software and Documentation were developed at private expense and
 * are "Commercial Items" as that term is defined at 48 CFR 2.101,
 * consisting of "Commercial Computer Software" and
 * "Commercial Computer Software Documentation", as such terms are
 * used in 48 CFR 12.212 or 48 CFR 227.7202-1 through 227.7202-4,
 * as applicable.
 */

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.io.IOException;

import ilog.views.IlvGraphic;
import ilog.views.IlvGraphicBag;
import ilog.views.IlvGraphicUtil;
import ilog.views.IlvManager;
import ilog.views.IlvPoint;
import ilog.views.IlvRect;
import ilog.views.IlvStroke;
import ilog.views.IlvTransformer;
import ilog.views.graphic.IlvGraphicSet;
import ilog.views.graphic.IlvPolyPoints;
import ilog.views.io.IlvFieldNotFoundException;
import ilog.views.io.IlvInputStream;
import ilog.views.io.IlvOutputStream;
import ilog.views.io.IlvReadFileException;
import ilog.views.maps.IlvCoordinateSystemProperty;
import ilog.views.maps.IlvLinearUnit;
import ilog.views.maps.IlvScaleController;
import ilog.views.maps.projection.IlvProjectionUtil;
import ilog.views.maps.srs.coordsys.IlvCoordinateSystem;

class LineInfo {
  float len;
  float ddx;
  float ddy;
  float x1;
  float y1;
  float x2;
  float y2;

  float l1x1;
  float l1y1;
  float l1x2;
  float l1y2;

  float l2x1;
  float l2y1;
  float l2x2;
  float l2y2;

  IlvPoint p1 = new IlvPoint();
  IlvPoint p2 = new IlvPoint();

  float _ix;
  float _iy;

  boolean doJoin;
  float jx1;
  float jy1;
  float jx2;
  float jy2;

  static final float SMALL = 1e-10f;

  LineInfo() {
  }

  void create(IlvPoint p[], int i, IlvTransformer t, float spacing) {
    p1.x = p[i].x;
    p1.y = p[i].y;
    i++;
    p2.x = p[i].x;
    p2.y = p[i].y;

    if (t != null) {
      t.apply(p1);
      t.apply(p2);
    }

    x1 = p1.x;
    y1 = p1.y;
    x2 = p2.x;
    y2 = p2.y;

    float dx = x2 - x1;
    float dy = y2 - y1;
    len = (float) Math.sqrt(dx * dx + dy * dy);

    ddx = spacing * dy / len;
    ddy = spacing * dx / len;

    l1x1 = x1 + ddx;
    l1y1 = y1 - ddy;
    l1x2 = x2 + ddx;
    l1y2 = y2 - ddy;

    l2x1 = x1 - ddx;
    l2y1 = y1 + ddy;
    l2x2 = x2 - ddx;
    l2y2 = y2 + ddy;
  }

  void join(float x11, float y11, float x21, float y21) {
    jx1 = x11;
    jy1 = y11;
    jx2 = x21;
    jy2 = y21;
    doJoin = true;
  }

  void intersect(LineInfo info) {
    doJoin = false;
    if (intersect(l1x1, l1y1, l1x2, l1y2, info.l1x1, info.l1y1, info.l1x2, info.l1y2)) {
      float x = _ix;
      float y = _iy;
      l1x2 = x;
      l1y2 = y;
      info.l1x1 = x;
      info.l1y1 = y;
    } else {
      join(l1x2, l1y2, info.l1x1, info.l1y1);
    }

    if (intersect(l2x1, l2y1, l2x2, l2y2, info.l2x1, info.l2y1, info.l2x2, info.l2y2)) {
      float x = _ix;
      float y = _iy;
      l2x2 = x;
      l2y2 = y;
      info.l2x1 = x;
      info.l2y1 = y;
    } else {
      join(l2x2, l2y2, info.l2x1, info.l2y1);
    }
  }

  boolean intersect(float x11, float y11, float x21, float y21, float x3, float y3, float x4, float y4) {
    float d = (y4 - y3) * (x21 - x11) - (x4 - x3) * (y21 - y11);
    if (Math.abs(d) == 0.)
      return false;

    float na = ((x4 - x3) * (y11 - y3) - (y4 - y3) * (x11 - x3)) / d;
    if (na < 0f || na > 1f)
      return false;
    float nb = ((x21 - x11) * (y11 - y3) - (y21 - y11) * (x11 - x3)) / d;
    if (nb < 0f || nb > 1f)
      return false;

    _ix = (int) (x11 + na * (x21 - x11));
    _iy = (int) (y11 + na * (y21 - y11));

    return true;
  }

  void affect(LineInfo source) {
    len = source.len;
    ddx = source.ddx;
    ddy = source.ddy;
    x1 = source.x1;
    y1 = source.y1;
    x2 = source.x2;
    y2 = source.y2;

    l1x1 = source.l1x1;
    l1y1 = source.l1y1;
    l1x2 = source.l1x2;
    l1y2 = source.l1y2;

    l2x1 = source.l2x1;
    l2y1 = source.l2y1;
    l2x2 = source.l2x2;
    l2y2 = source.l2y2;

    doJoin = false;
  }
}

/**
 * A graphical object representing a road/railroad. This object is configured by
 * another class, <code>IlvRailroadAttributes</code>.
 */
public class IlvRailroad extends IlvPolyPoints {
  private IlvRailroadAttributes _railAttributes;
  private LineInfo _current = new LineInfo();
  private LineInfo _prev = new LineInfo();
  private LineInfo _next = new LineInfo();
  private IlvPoint[] _tmp;
  private int _x[];
  private int _y[];
  BasicStroke _basicStroke;
  BasicStroke _resetStroke = new BasicStroke();

  public IlvRailroad(IlvPoint[] points, boolean copy) {
    super(points, copy);
  }

  public IlvRailroad(IlvPoint[] points, boolean copy, IlvRailroadAttributes a) {
    super(points, copy);
    setRailroadAttributes(a);
  }

  public IlvRailroad(IlvRailroad source) {
    super(source);
    if (source._railAttributes != null)
      source.setRailroadAttributes(source._railAttributes.copy());
  }

  public IlvRailroad(IlvInputStream stream) throws IlvReadFileException {
    super(stream);
    try {
      _railAttributes = (IlvRailroadAttributes) stream.readPersistentObject("railAttributes");
    } catch (IlvFieldNotFoundException e) {
    }
  }

  public void write(IlvOutputStream stream) throws IOException {
    super.write(stream);
    if (_railAttributes != null)
      stream.write("railAttributes", _railAttributes);
  }

  public IlvGraphic copy() {
    return new IlvRailroad(this);
  }

  public IlvRailroadAttributes getRailroadAttributes() {
    if (_railAttributes == null)
      _railAttributes = new IlvRailroadAttributes();
    return _railAttributes;
  }

  public void setRailroadAttributes(IlvRailroadAttributes ra) {
    _railAttributes = ra;
  }

  private Double _factor;

  private double getZoomFactorFromScale(double scale) {
    if (_factor != null)
      return _factor.doubleValue();
    IlvManager manager = null;
    IlvGraphicBag b = getGraphicBag();
    while (b instanceof IlvGraphicSet)
      b = ((IlvGraphic) b).getGraphicBag();
    if (b instanceof IlvManager)
      manager = (IlvManager) b;
    double tFactor = 1.;
    if (manager != null) {
      IlvCoordinateSystem cs = IlvCoordinateSystemProperty.GetCoordinateSystem(manager);
      if (cs == null)
        tFactor = 1.;
      else {
        IlvLinearUnit unit = IlvProjectionUtil.GetLinearUnit(cs);
        tFactor = unit.toMeters(scale) * 1000f;
        tFactor /= IlvScaleController.GetScreenPixelSizeMM();
      }
    }
    _factor = new Double(tFactor);
    return tFactor;
  }

  public float getRailSpacing(IlvTransformer t) {
    IlvRailroadAttributes ra = getRailroadAttributes();
    float val = ra.getRailSpacing();
    float max = ra.getMaximumRailSpacing();
    float scale = ra.getScale();
    double zl = getZoomFactorFromScale(scale);

    if (max == 0)
      return val;
    float zoomlevel = (t == null ? 1f : (float) t.zoomFactor());
    double v = zoomlevel * val / zl;
    if (v >= max)
      v = max;
    return (float) v;
  }

  public float getTieWidth(IlvTransformer t) {
    IlvRailroadAttributes ra = getRailroadAttributes();
    float val = ra.getTieWidth();
    float max = ra.getMaximumTieWidth();
    float scale = ra.getScale();
    double zl = getZoomFactorFromScale(scale);

    if (max == 0)
      return val;

    float zoomlevel = (t == null ? 1f : (float) t.zoomFactor());
    double v = zoomlevel * val / zl;

    if (v >= max)
      v = max;
    return (float) v;
  }

  public IlvRect boundingBox(IlvTransformer t) {
    IlvRect box;
    box = super.boundingBox(t);
    getRailroadAttributes();
    float rs = getRailSpacing(t);
    float tw = getTieWidth(t);
    float width = (float) Math.max(tw, rs);
    box.expand(width / 2f);
    return box;
  }

  public boolean pointsInBBox() {
    return true;
  }

  public boolean contains(IlvPoint p, IlvPoint tp, IlvTransformer t) {
    float zoom = t == null ? 1f : (float) t.zoomFactor();
    IlvPoint pts[] = getPoints();
    getRailroadAttributes();
    float tw = getTieWidth(t);
    float rs = getRailSpacing(t);
    float width = (float) Math.max(tw, rs);
    return IlvGraphicUtil.PointInPolyline(tp, pts, pts.length, width / zoom, IlvStroke.JOIN_MITER, IlvStroke.CAP_BUTT,
        t);
  }

  private void initTmpPoints() {
    if (_tmp != null)
      return;
    _tmp = new IlvPoint[3];
    _tmp[0] = new IlvPoint();
    _tmp[1] = new IlvPoint();
    _tmp[2] = new IlvPoint();
  }

  private void initTmpXYPoints() {
    if (_x == null)
      _x = new int[5];
    if (_y == null)
      _y = new int[5];
  }

  float drawTies(Graphics dst, IlvTransformer t, float offset, LineInfo current, LineInfo prev, LineInfo next) {
    float d = current.len;
    if (offset > d)
      return offset - d;
    IlvRailroadAttributes ra = getRailroadAttributes();
    float tieSpacing = ra.getTieSpacing() * 2f;
    float tieWidth = getTieWidth(t);
    float tt = ra.getTieThickness();
    if (tt > 0f)
      initTmpPoints();
    dst.setColor(ra.getTieColor());

    float x1 = current.x1;
    float y1 = current.y1;
    float dx = current.x2 - x1;
    float dy = current.y2 - y1;
    float ddx = tieWidth * dy / d;
    float ddy = tieWidth * dx / d;

    float u;
    for (u = offset; u < d; u += tieSpacing) {
      float ud = u / d;
      float px = x1 + ud * dx;
      float py = y1 + ud * dy;
      if (!dst.hitClip((int) (px - tieWidth) - 1, (int) (py - tieWidth) - 1, (int) (2f * tieWidth) + 2,
          (int) (2f * tieWidth) + 2))
        continue;
      float fx = px - ddx;
      float fy = py + ddy;
      float tx = px + ddx;
      float ty = py - ddy;

      float limit = ra.getSlantingLimit();
      if (limit > 0f) {
        if ((next != null) && ((d - u) <= limit)) {
          float angle = (angle(current, next) - (float) Math.PI) / 2f;
          angle *= 1f - (d - u) / limit;
          float sa = -(float) Math.sin(angle);
          float ca = (float) Math.cos(angle);
          fx = -ddx * ca - ddy * sa + px;
          fy = -ddx * sa + ddy * ca + py;
          tx = ddx * ca + ddy * sa + px;
          ty = ddx * sa - ddy * ca + py;
        }

        if ((prev != null) && (u <= limit)) {
          float angle = (angle(prev, current) - (float) Math.PI) / 2f;
          angle *= 1f - u / limit;
          float sa = (float) Math.sin(angle);
          float ca = (float) Math.cos(angle);
          fx = -ddx * ca - ddy * sa + px;
          fy = -ddx * sa + ddy * ca + py;
          tx = ddx * ca + ddy * sa + px;
          ty = ddx * sa - ddy * ca + py;
        }
      }
      if (tt > 0f) {
        _tmp[0].move(fx + 0.5f, fy + 0.5f);
        _tmp[1].move(tx + 0.5f, ty + 0.5f);
        IlvGraphicUtil.DrawPolyline(dst, _tmp, 2, tt, IlvStroke.JOIN_MITER, IlvStroke.CAP_BUTT, null, null);
      } else
        dst.drawLine((int) (fx), (int) (fy), (int) (tx), (int) (ty));
    }
    return (u - d);
  }

  float angle(LineInfo l1, LineInfo l2) {
    float dx1 = l1.x1 - l1.x2;
    float dy1 = l1.y1 - l1.y2;
    float dx2 = l2.x2 - l2.x1;
    float dy2 = l2.y2 - l2.y1;
    float dp = dx1 * dx2 + dy1 * dy2;
    float cos = dp / (l1.len * l2.len);
    if (cos < -1f)
      cos = -1;
    if (cos > 1f)
      cos = 1f;
    float angle = (float) Math.acos(cos);
    float cp = dx1 * dy2 - dx2 * dy1;
    if (cp > 0)
      angle = 2f * (float) Math.PI - angle;
    return angle;
  }

  void drawBackground(Graphics dst, LineInfo info) {
    IlvRailroadAttributes ra = getRailroadAttributes();
    Color b = ra.getBackground();
    if (b != null) {
      initTmpXYPoints();
      int i = 0;
      _x[i] = (int) (info.l1x1);
      _y[i] = (int) (info.l1y1);
      i++;
      _x[i] = (int) (info.l1x2);
      _y[i] = (int) (info.l1y2);
      i++;
      if (info.doJoin) {
        _x[i] = (int) (info.jx2);
        _y[i] = (int) (info.jy2);
        i++;
      }
      _x[i] = (int) (info.l2x2);
      _y[i] = (int) (info.l2y2);
      i++;
      _x[i] = (int) (info.l2x1);
      _y[i] = (int) (info.l2y1);
      i++;
      dst.setColor(b);
      dst.fillPolygon(_x, _y, i);
    }
  }

  void drawRails(Graphics dst, LineInfo info) {
    IlvRailroadAttributes ra = getRailroadAttributes();
    dst.setColor(ra.getRailColor());
    dst.drawLine((int) (info.l1x1), (int) (info.l1y1), (int) (info.l1x2), (int) (info.l1y2));
    dst.drawLine((int) (info.l2x1), (int) (info.l2y1), (int) (info.l2x2), (int) (info.l2y2));
    if (info.doJoin)
      dst.drawLine((int) (info.jx1), (int) (info.jy1), (int) (info.jx2), (int) (info.jy2));
  }

  void drawCenterLine(Graphics dst, LineInfo info) {
    IlvRailroadAttributes ra = getRailroadAttributes();
    Color c = ra.getCenterLineColor();
    dst.setColor(c);
    float lw = ra.getCenterLineWidth();
    boolean stroked = false;
    if (lw > 1.0) {
      if ((_basicStroke == null) || (_basicStroke.getLineWidth() != lw))
        _basicStroke = new BasicStroke(lw);
      stroked = true;
      ((Graphics2D) dst).setStroke(_basicStroke);
    }
    dst.drawLine((int) (info.x1), (int) (info.y1), (int) (info.x2), (int) (info.y2));
    if (stroked)
      ((Graphics2D) dst).setStroke(_resetStroke);
  }

  public void draw(Graphics dst, IlvTransformer t) {
    IlvPoint pts[] = getPoints();
    if (pts.length < 2)
      return;
    IlvRailroadAttributes ra = getRailroadAttributes();
    float railSpacing = getRailSpacing(t);
    float tieWidth = getTieWidth(t);
    boolean doRails = !ra.isDrawingOneLine();
    if (railSpacing < 1f)
      doRails = false;
    boolean doTies = ra.isDrawingTies();
    if (tieWidth < 1f)
      doTies = false;
    boolean doCenterLine = ra.isDoingCenterLine();
    if (!doRails) {
      dst.setColor(ra.getRailColor());
      IlvGraphicUtil.DrawPolyline(dst, pts, pts.length, 0, IlvStroke.JOIN_MITER, IlvStroke.CAP_BUTT, null, t);
      if (!doTies)
        return;
    }
    _current.create(pts, 0, t, railSpacing);
    if (pts.length == 2) {
      if (doRails) {
        drawBackground(dst, _current);
        if (doCenterLine)
          drawCenterLine(dst, _current);
        drawRails(dst, _current);
      }
      if (doTies)
        drawTies(dst, t, 0f, _current, null, null);
      return;
    }
    float offset = 0f;
    for (int i = 1; i < pts.length - 1; i++) {
      _next.create(pts, i, t, railSpacing);
      _current.intersect(_next);
      if (doRails) {
        drawBackground(dst, _current);
        if (doCenterLine)
          drawCenterLine(dst, _current);
        drawRails(dst, _current);
      }
      if (doTies)
        offset = drawTies(dst, t, offset, _current, i == 1 ? null : _prev, _next);
      _prev.affect(_current);
      _current.affect(_next);
    }
    if (doRails) {
      drawBackground(dst, _current);
      if (doCenterLine)
        drawCenterLine(dst, _current);
      drawRails(dst, _current);
    }
    if (doTies)
      drawTies(dst, t, offset, _current, _prev, null);
  }

}