/**
 *    Copyright 2011 Peter Murray-Rust et. al.
 *
 *    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.xmlcml.cml.element;



import nu.xom.Element;
import org.apache.log4j.Logger;
import org.xmlcml.cml.base.CMLElement;
import org.xmlcml.euclid.EuclidRuntimeException;
import org.xmlcml.euclid.Line3;
import org.xmlcml.euclid.Plane3;
import org.xmlcml.euclid.Point3;
import org.xmlcml.euclid.Util;
import org.xmlcml.euclid.Vector3;

/**
 * user-modifiable class supporting plane3. * autogenerated from schema use as a
 * shell which can be edited
 *
 */
public class CMLPlane3 extends AbstractPlane3 {

	/** namespaced element name.*/
	public final static String NS = C_E+TAG;

    private final static Logger LOG = Logger.getLogger(CMLPlane3.class.getName());

    /**
     * default. do not use.
     * public because of newInstance()
     */
    public CMLPlane3() {
    }

    /**
     * constructor.
     *
     * @param old
     */
    public CMLPlane3(CMLPlane3 old) {
        super((AbstractPlane3) old);

    }

    /**
     * copy node .
     *
     * @return Node
     */
    public Element copy() {
        return new CMLPlane3(this);

    }

    /**
     * create new instance in context of parent, overridable by subclasses.
     *
     * @param parent
     *            parent of element to be constructed (ignored by default)
     * @return CMLPlane3
     */
    public CMLElement makeElementInContext(Element parent) {
        return new CMLPlane3();

    }

    /**
     * check plane is OK. must have 3 double components
     *
     * @param parent
     *            element
     * @throws RuntimeException
     *             parsing error
     */
    public void finishMakingElement(Element parent) throws RuntimeException {
        double[] array = this.getXMLContent();
        if (array == null) {
            LOG.warn("plane must not be empty");
        } else if (array.length != 4) {
        	LOG.warn("plane must have 4 double components");
        }
    }

    // =========================== additional constructors
    // ========================

    /**
     * formed from components.
     *
     * @param array
     *            4-component
     */
    public CMLPlane3(double[] array) {
        this.setArray(array);
    }

    /**
     * formed from euclid plane.
     *
     * @param plane
     */
    public CMLPlane3(Plane3 plane) {
        this();
        setArrayNoCheck(plane.getArray());
    }

    /**
     * formed from separate components.
     *
     * @param lmn
     *            component
     * @param d
     *            distance
     * @throws RuntimeException
     */
    public CMLPlane3(double[] lmn, double d) {
        this();
        try {
            Util.check(lmn, 3);
            Plane3 pp = new Plane3(lmn, d);
            this.setArrayNoCheck(pp.getArray());
        } catch (EuclidRuntimeException e) {
            throw new RuntimeException("" + e);
        }
    }

    /**
     * formed from vector and distance. takes doc from vector
     *
     * @param v
     *            vector
     * @param d
     *            distance
     * @throws RuntimeException
     */
    public CMLPlane3(CMLVector3 v, double d) throws RuntimeException {
        Plane3 pp;
        try {
            pp = new Plane3(v.getEuclidVector3(), d);
            this.setArrayNoCheck(pp.getArray());
        } catch (Exception e) {
            throw new RuntimeException("" + e);
        }
    }

    /**
     * make a plane from three points. takes doc from points
     *
     * @param p1
     *            point
     * @param p2
     *            point
     * @param p3
     *            point
     */
    public CMLPlane3(CMLPoint3 p1, CMLPoint3 p2, CMLPoint3 p3) {
        Plane3 pp= new Plane3(p1.getEuclidPoint3(), p2.getEuclidPoint3(), p3
                    .getEuclidPoint3());
        this.setArrayNoCheck(pp.getArray());
    }

    /**
     * make a plane from a line and a point not on the line. takes doc from line
     *
     * @param l
     *            point
     * @param p
     *            point
     */
    public CMLPlane3(CMLLine3 l, CMLPoint3 p) {
        Plane3 pp = new Plane3(l.getEuclidLine3(), p.getEuclidPoint3());
        this.setArrayNoCheck(pp.getArray());
    }

    // ====================== housekeeping methods =====================

    // assumes plane3 != null
    /**
     * get euclid primitive.
     *
     * @return plane
     * @exception RuntimeException
     */
    public Plane3 getEuclidPlane3() throws RuntimeException {
        Plane3 pleucl3 = null;
        try {
            pleucl3 = new Plane3(this.getXMLContent());
        } catch (EuclidRuntimeException e) {
            throw new RuntimeException("" + e);
        }
        return pleucl3;
    }

    // ====================== subsidiary accessors =====================

    /**
     * sets components.
     *
     * @param array
     *            4 components
     */
    public void setArray(double[] array) {
        Util.check(array, 4);
        setArrayNoCheck(array);
    }

    private void setArrayNoCheck(double[] array) throws RuntimeException {
        Vector3 v = new Vector3(array[0], array[1], array[2]);
        if (v.isZero()) {
            throw new RuntimeException("Cannot make plane with zero vector");
        }
        v.normalize();
        double[] vv = v.getArray();
        double[] aa = new double[4];
        aa[0] = vv[0];
        aa[1] = vv[1];
        aa[2] = vv[2];
        aa[3] = array[3];
        this.setXMLContent(aa);
    }

    /**
     * gets components.
     *
     * @return 4-component array
     */
    public double[] getArray() {
        Plane3 pleucl3 = this.getEuclidPlane3();
        return pleucl3.getArray();
    }

    /**
     * get vector.
     *
     * @return the vector
     */
    public CMLVector3 getVector() {
        Plane3 pleucl3 = this.getEuclidPlane3();
        return CMLVector3.createCMLVector3(pleucl3.getVector());
    }

    /**
     * get distance from origin.
     *
     * @return the distance
     */
    public double getDistance() {
        Plane3 pleucl3 = this.getEuclidPlane3();
        return pleucl3.getArray()[3];
    }

    // ====================== functionality =====================

    /**
     * reverse direction of plane.
     */
    public void negative() {
        Plane3 pleucl3 = this.getEuclidPlane3();
        pleucl3.negative();
        this.setXMLContent(pleucl3.getArray());
    }

    /**
     * are two planes coincident and parallel.
     *
     * @param pl2
     *            plane to compare
     * @return true if equal within Real.isEqual()
     */
    public boolean isEqualTo(CMLPlane3 pl2) {
        Plane3 pleucl3 = this.getEuclidPlane3();
        return pleucl3.isEqualTo(pl2.getEuclidPlane3());
    }

    /**
     * form coincident antiparallel plane.
     *
     * @return antiparallel plane
     */
    public CMLPlane3 subtract() {
        Plane3 pleucl3 = this.getEuclidPlane3();
        return new CMLPlane3(pleucl3.subtract());
    }

    /**
     * distance of point from plane. will be a signed quantity
     *
     * @param p
     *            the point
     * @return the distance
     */
    public double getDistanceFromPoint(CMLPoint3 p) {
        Plane3 pleucl3 = this.getEuclidPlane3();
        return pleucl3.getDistanceFromPoint(p.getEuclidPoint3());
    }

    /**
     * are two planes parallel. not antiparallel
     *
     * @param pl2
     *            the plane
     * @return true if parallel within Real.isEqual()
     */
    public boolean isParallelTo(CMLPlane3 pl2) {
        Plane3 pleucl3 = this.getEuclidPlane3();
        return pleucl3.isParallelTo(pl2.getEuclidPlane3());
    }

    /**
     * are two planes antiparallel. not parallel
     *
     * @param pl2
     *            the plane
     * @return true if antiparallel within Real.isEqual()
     */
    public boolean isAntiparallelTo(CMLPlane3 pl2) {
        Plane3 pleucl3 = this.getEuclidPlane3();
        return pleucl3.isAntiparallelTo(pl2.getEuclidPlane3());
    }

    /**
     * is a point on the plane.
     *
     * @param p
     *            the point
     * @return true if within Real.isEqual()
     */
    public boolean containsPoint(CMLPoint3 p) {
        double d = this.getDistanceFromPoint(p);
        return (Math.abs(d) < EPS);
    }

    /**
     * point on plane closest to another point. if p2 is on plane then result
     * will coincide
     *
     * @param p2
     *            other point
     * @return the closest point
     */
    public CMLPoint3 getClosestPointTo(CMLPoint3 p2) {
        Plane3 pleucl3 = this.getEuclidPlane3();
        Point3 p = pleucl3.getClosestPointTo(p2.getEuclidPoint3());
        return new CMLPoint3(p);
    }

    /**
     * point of intersection of plane and line.
     *
     * @param l
     *            line
     * @return intersection point
     */
    public CMLPoint3 getIntersectionWith(CMLLine3 l) {
        Plane3 pleucl3 = this.getEuclidPlane3();
        Point3 p = pleucl3.getIntersectionWith(l.getEuclidLine3());
        return (p == null) ? null : new CMLPoint3(p);
    }

    /**
     * get line as intersection of two planes.
     *
     * @param pl2
     *            plane
     * @return intersection line
     */
    public CMLLine3 getIntersectionWith(CMLPlane3 pl2) {
        Plane3 pleucl3 = this.getEuclidPlane3();
        Line3 l = pleucl3.getIntersectionWith(pl2.getEuclidPlane3());
        return (l == null) ? null : new CMLLine3(l);
    }

    /**
     * point where three planes intersect
     *
     * @param pl2
     *            plane
     * @param pl3
     *            plane
     * @return intersection point
     */
    public CMLPoint3 getIntersectionWith(CMLPlane3 pl2, CMLPlane3 pl3) {
        Plane3 pleucl3 = this.getEuclidPlane3();
        Point3 p = pleucl3.getIntersectionWith(pl2.getEuclidPlane3(), pl3
                .getEuclidPlane3());
        return (p == null) ? null : new CMLPoint3(p);
    }

    /**
     * the angle between 2 planes.
     *
     * @param pl2
     *            plane
     * @return the angle (unsigned radians)
     */
    public double getAngleMadeWith(CMLPlane3 pl2) {
        Plane3 pleucl3 = this.getEuclidPlane3();
        return pleucl3.getAngleMadeWith(pl2.getEuclidPlane3()).getRadian();
    }

}
