/*
* Copyright 2014 Basis Technology Corp.
*
* 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.basistech.tclre;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Nfa representation.
 */
class Nfa {
    private static final Logger LOG = LoggerFactory.getLogger(Nfa.class);
    private static final boolean IS_DEBUG = System.getProperty("tclre.debug") != null;

    private static final int INCOMPATIBLE = 1;
    private static final int SATISFIED = 2;
    private static final int COMPATIBLE = 3;

    final State pre;  /* pre-initial state */
    final State init; /* initial state */
    final State finalState;   /* final state */
    final State post; /* post-final state */
    final ColorMap cm;    /* the color map */
    final short[] bos = new short[2];     /* colors, if any, assigned to BOS and BOL */
    final short[] eos = new short[2];     /* colors, if any, assigned to EOS and EOL */
    //
    final Nfa parent; /* parent NFA, if any */
    private State states;   /* state-chain header */
    private State slast;    /* tail of the chain */
    private int nstates;        /* for numbering states */

    /**
     * New Nfa at the top level.
     *
     * @param cm
     */
    @SuppressWarnings("PMD")
    Nfa(ColorMap cm) {
        this(null, cm);
    }

    @SuppressWarnings("PMD")
    Nfa(Nfa parent) {
        this(parent, parent.cm);
    }

    private Nfa(Nfa parent, ColorMap cm) {
        this.parent = parent;
        this.cm = cm;
        nstates = 0;
        bos[1] = Constants.COLORLESS;
        bos[0] = bos[1];
        eos[1] = Constants.COLORLESS;
        eos[0] = eos[1];
        post = newstate('@');   /* number 0 */
        pre = newstate('>');        /* number 1 */

        init = newstate();      /* may become invalid later */
        finalState = newstate();
        cm.rainbow(this, Compiler.PLAIN, Constants.COLORLESS, pre, init);
        newarc('^', (short)1, pre, init);
        newarc('^', (short)0, pre, init);
        cm.rainbow(this, Compiler.PLAIN, Constants.COLORLESS, finalState, post);
        newarc('$', (short)1, finalState, post);
        newarc('$', (short)0, finalState, post);
    }

    void newarc(int t, short co, State from, State to) {
        Arc a;

        assert from != null && to != null;

    /* check for duplicates */
        for (a = from.outs; a != null; a = a.outchain) {
            if (a.co == co && a.to == to && a.type == t) {
                return;
            }
        }

        a = new Arc(t, co, from, to);

    /*
     * Put the new arc on the beginning, not the end, of the chains.
     * Not only is this easier, it has the very useful side effect that
     * deleting the most-recently-added arc is the cheapest case rather
     * than the most expensive one.
     */
        a.inchain = to.ins;
        to.ins = a;
        a.outchain = from.outs;
        from.outs = a;

        from.nouts++;
        to.nins++;

        if (a.colored() && parent == null) {
            cm.colorchain(a);
        }
    }

    void emptyarc(State from, State to) {
        newarc(Compiler.EMPTY, (short)0, from, to);
    }

    /**
     * Factory method for new states.
     *
     * @return a new state wired into this Nfa.
     */
    State newstate(int flag) {
        State newState = new State();
        newState.no = nstates++; // a unique number.
        if (states == null) {
            states = newState;
        }
        if (slast != null) {
            assert slast.next == null;
            slast.next = newState;
        }
        newState.prev = slast;
        slast = newState;
        newState.flag = flag;
        return newState;
    }

    /**
     * moveouts - move all out arcs of a state to another state
     */
    void moveouts(State old, State newState) {
        Arc a;

        assert old != newState;

        while ((a = old.outs) != null) {
            cparc(a, newState, a.to);
            freearc(a);
        }
    }

    /**
     * moveins - move all in arcs of a state to another state
     * You might think this could be done better by just updating the
     * existing arcs, and you would be right if it weren't for the desire
     * for duplicate suppression, which makes it easier to just make new
     * ones to exploit the suppression built into newarc.
     */
    void moveins(State old, State newState) {
        Arc a;

        assert old != newState;

        while ((a = old.ins) != null) {
            cparc(a, a.from, newState);
            freearc(a);
        }
        assert old.nins == 0;
        assert old.ins == null;
    }

    /**
     * copyins - copy all in arcs of a state to another state
     */
    void copyins(State old, State newState) {
        Arc a;

        assert old != newState;

        for (a = old.ins; a != null; a = a.inchain) {
            cparc(a, a.from, newState);
        }
    }

    /**
     * copyouts - copy all out arcs of a state to another state
     */
    void copyouts(State old, State newState) {
        Arc a;

        assert old != newState;

        for (a = old.outs; a != null; a = a.outchain) {
            cparc(a, newState, a.to);
        }
    }

    /**
     * Convenience method to return a new state with flag = 0.
     *
     * @return new state
     */
    State newstate() {
        return newstate(0);
    }

    /**
     * get rid of a state, releasing all its arcs.
     * I'm not sure that all this is needed, as opposed to depending on the GC.
     *
     * @param s the state to dispose of.
     */
    void dropstate(State s) {
        Arc a;

        while ((a = s.ins) != null) {
            freearc(a);
        }

        while ((a = s.outs) != null) {
            freearc(a);
        }

        freestate(s);
    }

    /**
     * Unwire a state from the NFA.
     *
     * @param s the state
     */
    void freestate(State s) {
        assert s != null;
        assert s.nins == 0;
        assert s.nouts == 0;
        if (s.next != null) {
            s.next.prev = s.prev;
        } else {
            assert s == slast;
            slast = s.prev;
        }
        if (s.prev != null) {
            s.prev.next = s.next;
        } else {
            assert s == states;
            states = s.next;
        }
    }

    void freearc(Arc victim) {
        State from = victim.from;
        State to = victim.to;
        assert victim.type != 0;

        // only maintain colorchain on top-level NFA
        if (victim.colored() && parent == null) {
            cm.uncolorchain(victim);
        }

        assert from != null;
        assert from.outs != null;
        Arc a = from.outs;
        if (a == victim) { // first in chain
            from.outs = victim.outchain;
        } else {
            for (; a != null && a.outchain != victim; a = a.outchain) {
                continue;
            }
            assert a != null;
            a.outchain = victim.outchain;
        }
        from.nouts--;

           /* take it off target's in-chain */
        assert to != null;
        assert to.ins != null;
        a = to.ins;
        if (a == victim) {      /* simple case:  first in chain */
            to.ins = victim.inchain;
        } else {
            for (; a != null && a.inchain != victim; a = a.inchain) {
                continue;
            }
            assert a != null;
            a.inchain = victim.inchain;
        }
        to.nins--;
    }

    /**
     * cparc - allocate a new arc within an NFA, copying details from old one
     */
    void cparc(Arc oa, State from, State to) {
        newarc(oa.type, oa.co, from, to);
    }

    /**
     * dupnfa - duplicate sub-NFA
     * Another recursive traversal, this time using tmp to point to duplicates
     * as well as mark already-seen states.  (You knew there was a reason why
     * it's a state pointer, didn't you? :-))
     *
     * @param start duplicate starting here
     * @param stop  ending here.
     * @param from  stringing dup from here
     * @param to    here.
     */
    void dupnfa(State start, State stop, State from, State to) {
        if (start == stop) {
            newarc(Compiler.EMPTY, (short)0, from, to);
            return;
        }

        stop.tmp = to;
        duptraverse(start, from);
    /* done, except for clearing out the tmp pointers */

        stop.tmp = null;
        cleartraverse(start);
    }

    /**
     * duptraverse - recursive heart of dupnfa
     */
    void duptraverse(State s, State stmp) {
        Arc a;

        if (s.tmp != null) {
            return;     /* already done */
        }

        s.tmp = (stmp == null) ? newstate() : stmp;
        if (s.tmp == null) {
            return;
        }

        for (a = s.outs; a != null; a = a.outchain) {
            duptraverse(a.to, null);
            assert a.to.tmp != null;
            cparc(a, s.tmp, a.to.tmp);
        }
    }

    /*
     - cleartraverse - recursive cleanup for algorithms that leave tmp ptrs set
     */
    void cleartraverse(State s) {
        Arc a;

        if (s.tmp == null) {
            return;
        }
        s.tmp = null;

        for (a = s.outs; a != null; a = a.outchain) {
            cleartraverse(a.to);
        }
    }

    /**
     * specialcolors - fill in special colors for an NFA
     */
    void specialcolors() {
    /* false colors for BOS, BOL, EOS, EOL */
        if (parent == null) {
            bos[0] = cm.pseudocolor();
            bos[1] = cm.pseudocolor();
            eos[0] = cm.pseudocolor();
            eos[1] = cm.pseudocolor();
        } else {
            assert parent.bos[0] != Constants.COLORLESS;
            bos[0] = parent.bos[0];
            assert parent.bos[1] != Constants.COLORLESS;
            bos[1] = parent.bos[1];
            assert parent.eos[0] != Constants.COLORLESS;
            eos[0] = parent.eos[0];
            assert parent.eos[1] != Constants.COLORLESS;
            eos[1] = parent.eos[1];
        }
    }

    /**
     * dumpnfa - dump an NFA in human-readable form
     */
    void dumpnfa() {
        if (!LOG.isDebugEnabled() || !IS_DEBUG) {
            return;
        }

        LOG.debug("dump nfa");

        StringBuilder sb = new StringBuilder();
        sb.append(String.format("pre %d, post %d init %d final %d", pre.no, post.no, init.no, finalState.no));

        if (bos[0] != Constants.COLORLESS) {
            sb.append(String.format(", bos [%d]", bos[0]));
        }
        if (bos[1] != Constants.COLORLESS) {
            sb.append(String.format(", bol [%d]", bos[1]));
        }
        if (eos[0] != Constants.COLORLESS) {
            sb.append(String.format(", eos [%d]", eos[0]));
        }
        if (eos[1] != Constants.COLORLESS) {
            sb.append(String.format(", eol [%d]", eos[1]));
        }
        LOG.debug(sb.toString());

        for (State s = states; s != null; s = s.next) {
            dumpstate(s);
        }
        if (parent == null) {
            cm.dumpcolors();
        }
    }

    /**
     * dumpstate - dump an NFA state in human-readable form
     */
    void dumpstate(State s) {
        Arc a;

        if (!LOG.isDebugEnabled() || !IS_DEBUG) {
            return;
        }

        StringBuilder sb = new StringBuilder();
        sb.append(String.format("State %d%s%c", s.no, (s.tmp != null) ? "T" : "",
                (s.flag != 0) ? (char)s.flag : '.'));
        if (s.prev != null && s.prev.next != s) {
            sb.append(String.format("\tstate chain bad"));
        }
        if (s.nouts == 0) {
            sb.append("\tno out arcs");
        } else {
            dumparcs(s, sb);
        }

        LOG.debug(sb.toString());
        for (a = s.ins; a != null; a = a.inchain) {
            if (a.to != s) {
                LOG.debug(String.format("\tlink from %d to %d on %d's in-chain",
                        a.from.no, a.to.no, s.no));
            }
        }
    }

    /**
     * dumparcs - dump out-arcs in human-readable form
     */
    void dumparcs(State s, StringBuilder sb) {
        int pos;

        assert s.nouts > 0;
    /* printing arcs in reverse order is usually clearer */
        pos = dumprarcs(s.outs, s, 1, sb);
        if (pos != 1) {
            //sb.append("\n");
        }
    }

    /**
     * dumprarcs - dump remaining outarcs, recursively, in reverse order
     *
     * @return resulting print position
     */
    int dumprarcs(Arc a, State s, int pos, StringBuilder sb) {
        if (a.outchain != null) {
            pos = dumprarcs(a.outchain, s, pos, sb);
        }
        dumparc(a, s, sb);
        if (pos == 5) {
            sb.append("\n");
            pos = 1;
        } else {
            pos++;
        }
        return pos;
    }

    /**
     * dumparc - dump one outarc in readable form, including prefixing tab
     */
    void dumparc(Arc a, State s, StringBuilder sb) {

        sb.append("\t");
        switch (a.type) {
        case Compiler.PLAIN:
            sb.append(String.format("[%d]", a.co));
            break;
        case Compiler.AHEAD:
            sb.append(String.format(">%d>", a.co));
            break;
        case Compiler.BEHIND:
            sb.append(String.format("<%d<", a.co));
            break;
        case Compiler.LACON:
            sb.append(String.format(":%d:", a.co));
            break;
        case '^':
        case '$':
            sb.append(String.format("%c%d", (char)a.type, a.co));
            break;
        case Compiler.EMPTY:
            break;
        default:
            sb.append(String.format("0x%x/0%d", a.type, a.co));
            break;
        }
        if (a.from != s) {
            sb.append(String.format("?%d?", a.from.no));
        }
        sb.append("->");
        if (a.to == null) {
            sb.append("null");
            Arc aa;
            for (aa = a.to.ins; aa != null; aa = aa.inchain) {
                if (aa == a) {
                    break;      /* NOTE BREAK OUT */
                }
            }
            if (aa == null) {
                LOG.debug("?!?");   /* missing from in-chain */
            }
        } else {
            sb.append(String.format("%d", a.to.no));
        }
    }

    /**
     * optimize - optimize an NFA
     *
     * @return re_info bits
     */
    long optimize() throws RegexException {
        LOG.debug("initial cleanup");
        cleanup();      /* may simplify situation */
        dumpnfa();
        LOG.debug("empties");
        fixempties();   /* get rid of EMPTY arcs */
        LOG.debug("constraints");
        pullback(); /* pull back constraints backward */
        pushfwd();  /* push fwd constraints forward */
        LOG.debug("final cleanup");
        cleanup();      /* final tidying */
        return analyze();   /* and analysis */
    }

    /**
     * analyze - ascertain potentially-useful facts about an optimized NFA
     *
     * @return re_info bits.
     */
    long analyze() {
        Arc a;
        Arc aa;

        if (pre.outs == null) {
            return Flags.REG_UIMPOSSIBLE;
        }
        for (a = pre.outs; a != null; a = a.outchain) {
            for (aa = a.to.outs; aa != null; aa = aa.outchain) {
                if (aa.to == post) {
                    return Flags.REG_UEMPTYMATCH;
                }
            }
        }
        return 0;
    }

    /**
     * pullback - pull back constraints backward to (with luck) eliminate them
     */
    void pullback() throws RegexException {
        State s;
        State nexts;
        Arc a;
        Arc nexta;
        boolean progress;

    /* find and pull until there are no more */
        do {
            progress = false;
            for (s = states; s != null; s = nexts) {
                nexts = s.next;
                for (a = s.outs; a != null; a = nexta) {
                    nexta = a.outchain;
                    if (a.type == '^' || a.type == Compiler.BEHIND) {
                        if (pull(a)) {
                            progress = true;
                        }
                    }
                    assert nexta == null || s.no != State.FREESTATE;
                }
            }
            if (progress) {
                dumpnfa();
            }
        } while (progress);

        for (a = pre.outs; a != null; a = nexta) {
            nexta = a.outchain;
            if (a.type == '^') {
                assert a.co == 0 || a.co == 1;
                newarc(Compiler.PLAIN, bos[a.co], a.from, a.to);
                freearc(a);
            }
        }
    }

    /**
     * - pull - pull a back constraint backward past its source state
     * A significant property of this function is that it deletes at most
     * one state -- the constraint's from state -- and only if the constraint
     * was that state's last outarc.
     *
     * @return success
     */
    boolean pull(Arc con) throws RegexException {
        State from = con.from;
        State to = con.to;
        Arc a;
        Arc nexta;
        State s;

        if (from == to) {   /* circular constraint is pointless */
            freearc(con);
            return true;
        }
        if (0 != from.flag) {   /* can't pull back beyond start */
            return false;
        }
        if (from.nins == 0) {   /* unreachable */
            freearc(con);
            return true;
        }

    /* first, clone from state if necessary to avoid other outarcs */
        if (from.nouts > 1) {
            s = newstate();

            assert to != from;        /* con is not an inarc */
            copyins(from, s);       /* duplicate inarcs */
            cparc(con, s, to);      /* move constraint arc */
            freearc(con);
            from = s;
            con = from.outs;
        }
        assert from.nouts == 1;

    /* propagate the constraint into the from state's inarcs */
        for (a = from.ins; a != null; a = nexta) {
            nexta = a.inchain;
            switch (combine(con, a)) {
            case INCOMPATIBLE:  /* destroy the arc */
                freearc(a);
                break;
            case SATISFIED:     /* no action needed */
                break;
            case COMPATIBLE:    /* swap the two arcs, more or less */
                s = newstate();
                cparc(a, s, to);        /* anticipate move */
                cparc(con, a.from, s);
                freearc(a);
                break;
            default:
                throw new RegexException("REG_ASSERT");
            }
        }

    /* remaining inarcs, if any, incorporate the constraint */
        moveins(from, to);
        dropstate(from);        /* will free the constraint */
        return true;
    }

    /**
     * pushfwd - push forward constraints forward to (with luck) eliminate them
     */
    void pushfwd() throws RegexException {
        State s;
        State nexts;
        Arc a;
        Arc nexta;
        boolean progress;

    /* find and push until there are no more */
        do {
            progress = false;
            for (s = states; s != null; s = nexts) {
                nexts = s.next;
                for (a = s.ins; a != null; a = nexta) {
                    nexta = a.inchain;
                    if (a.type == '$' || a.type == Compiler.AHEAD) {
                        if (push(a)) {
                            progress = true;
                        }
                    }
                    assert nexta == null || s.no != State.FREESTATE;
                }
            }
            if (progress) {
                dumpnfa();
            }
        } while (progress);

        for (a = post.ins; a != null; a = nexta) {
            nexta = a.inchain;
            if (a.type == '$') {
                assert a.co == 0 || a.co == 1;
                newarc(Compiler.PLAIN, eos[a.co], a.from, a.to);
                freearc(a);
            }
        }
    }

    /**
     * push - push a forward constraint forward past its destination state
     * A significant property of this function is that it deletes at most
     * one state -- the constraint's to state -- and only if the constraint
     * was that state's last inarc.
     */
    boolean push(Arc con) throws RegexException {
        State from = con.from;
        State to = con.to;
        Arc a;
        Arc nexta;
        State s;

        if (to == from) {   /* circular constraint is pointless */
            freearc(con);
            return true;
        }
        if (0 != to.flag) {     /* can't push forward beyond end */
            return false;
        }
        if (to.nouts == 0) {    /* dead end */
            freearc(con);
            return true;
        }

    /* first, clone to state if necessary to avoid other inarcs */
        if (to.nins > 1) {
            s = newstate();

            copyouts(to, s);        /* duplicate outarcs */
            cparc(con, from, s);    /* move constraint */
            freearc(con);
            to = s;
            con = to.ins;
        }
        assert to.nins == 1;

    /* propagate the constraint into the to state's outarcs */
        for (a = to.outs; a != null; a = nexta) {
            nexta = a.outchain;
            switch (combine(con, a)) {
            case INCOMPATIBLE:  /* destroy the arc */
                freearc(a);
                break;
            case SATISFIED:     /* no action needed */
                break;
            case COMPATIBLE:    /* swap the two arcs, more or less */
                s = newstate();
                cparc(con, s, a.to);    /* anticipate move */
                cparc(a, from, s);
                freearc(a);
                break;
            default:
                throw new RegexException("REG_ASSERT");
            }
        }

    /* remaining outarcs, if any, incorporate the constraint */
        moveouts(to, from);
        dropstate(to);      /* will free the constraint */
        return true;
    }

    /**
     * combine - constraint lands on an arc, what happens?
     *
     * @return result
     */
    int combine(Arc con, Arc a) throws RegexException {
        //# define  CA(ct,at)   (((ct)<<CHAR_BIT) | (at))

        //CA(con->type, a->type)) {
        switch ((con.type << 8) | a.type) {

        case '^' << 8 | Compiler.PLAIN:     /* newlines are handled separately */
        case '$' << 8 | Compiler.PLAIN:
            return INCOMPATIBLE;

        case Compiler.AHEAD << 8 | Compiler.PLAIN:      /* color constraints meet colors */
        case Compiler.BEHIND << 8 | Compiler.PLAIN:
            if (con.co == a.co) {
                return SATISFIED;
            }
            return INCOMPATIBLE;

        case '^' << 8 | '^':        /* collision, similar constraints */
        case '$' << 8 | '$':
        case Compiler.AHEAD << 8 | Compiler.AHEAD:
        case Compiler.BEHIND << 8 | Compiler.BEHIND:
            if (con.co == a.co) {       /* true duplication */
                return SATISFIED;
            }
            return INCOMPATIBLE;

        case '^' << 8 | Compiler.BEHIND:        /* collision, dissimilar constraints */
        case Compiler.BEHIND << 8 | '^':
        case '$' << 8 | Compiler.AHEAD:
        case Compiler.AHEAD << 8 | '$':
            return INCOMPATIBLE;

        case '^' << 8 | Compiler.AHEAD:
        case Compiler.BEHIND << 8 | '$':
        case Compiler.BEHIND << 8 | Compiler.AHEAD:
        case '$' << 8 | '^':
        case '$' << 8 | Compiler.BEHIND:
        case Compiler.AHEAD << 8 | '^':
        case Compiler.AHEAD << 8 | Compiler.BEHIND:
        case '^' << 8 | Compiler.LACON:
        case Compiler.BEHIND << 8 | Compiler.LACON:
        case '$' << 8 | Compiler.LACON:
        case Compiler.AHEAD << 8 | Compiler.LACON:
            return COMPATIBLE;
        default:
            throw new RuntimeException("Impossible arc");

        }
    }

    /**
     * cleanup - clean up NFA after optimizations
     */
    void cleanup() {
        State s;
        State nexts;
        int n;

    /* clear out unreachable or dead-end states */
    /* use pre to mark reachable, then post to mark can-reach-post */
        markreachable(pre, null, pre);
        markcanreach(post, pre, post);
        for (s = states; s != null; s = nexts) {
            nexts = s.next;
            if (s.tmp != post && 0 == s.flag) {
                dropstate(s);
            }
        }
        assert post.nins == 0 || post.tmp == post;

        cleartraverse(pre);

        assert post.nins == 0 || post.tmp == null;
    /* the nins==0 (final unreachable) case will be caught later */

    /* renumber surviving states */
        n = 0;
        for (s = states; s != null; s = s.next) {
            s.no = n++;
        }
        nstates = n;
    }

    /**
     * markreachable - recursive marking of reachable states
     *
     * @param s    a state
     * @param okay consider only states with this mark.
     * @param mark the value to mark with
     */
    void markreachable(State s, State okay, State mark) {
        Arc a;

        if (s.tmp != okay) {
            return;
        }

        s.tmp = mark;

        for (a = s.outs; a != null; a = a.outchain) {
            markreachable(a.to, okay, mark);
        }
    }

    /**
     * markcanreach - recursive marking of states which can reach here
     */
    void markcanreach(State s, State okay, State mark) {
        Arc a;

        if (s.tmp != okay) {
            return;
        }
        s.tmp = mark;

        for (a = s.ins; a != null; a = a.inchain) {
            markcanreach(a.from, okay, mark);
        }
    }

    /**
     * fixempties - get rid of EMPTY arcs
     */
    void fixempties() {
        State s;
        State nexts;
        Arc a;
        Arc nexta;
        boolean progress;

    /* find and eliminate empties until there are no more */
        do {
            progress = false;
            for (s = states; s != null; s = nexts) {
                nexts = s.next;
                for (a = s.outs; a != null; a = nexta) {
                    nexta = a.outchain;
                    if (a.type == Compiler.EMPTY && unempty(a)) {
                        progress = true;
                    }
                    assert nexta == null || s.no != State.FREESTATE;
                }
            }
            if (progress) {
                dumpnfa();
            }
        } while (progress);
    }

    /**
     * unempty - optimize out an EMPTY arc, if possible
     * Actually, as it stands this function always succeeds, but the return
     * value is kept with an eye on possible future changes.
     */
    boolean unempty(Arc a) {
        State from = a.from;
        State to = a.to;
        boolean usefrom;        /* work on from, as opposed to to? */

        assert a.type == Compiler.EMPTY;
        assert from != pre && to != post;

        if (from == to) {       /* vacuous loop */
            freearc(a);
            return true;
        }

    /* decide which end to work on */
        usefrom = true;         /* default:  attack from */
        if (from.nouts > to.nins) {
            usefrom = false;
        } else if (from.nouts == to.nins) {
        /* decide on secondary issue:  move/copy fewest arcs */
            if (from.nins > to.nouts) {
                usefrom = false;
            }
        }

        freearc(a);
        if (usefrom) {
            if (from.nouts == 0) {
            /* was the state's only outarc */
                moveins(from, to);
                freestate(from);
            } else {
                copyins(from, to);
            }
        } else {
            if (to.nins == 0) {
            /* was the state's only inarc */
                moveouts(to, from);
                freestate(to);
            } else {
                copyouts(to, from);
            }
        }

        return true;
    }

    Cnfa compact() {

        Arc a;
        int stateCount;
        int narcs;
        stateCount = 0;
        narcs = 0;

        for (State s = states; s != null; s = s.next) {
            stateCount++;
            narcs += 1 + s.nouts + 1;
            /* 1 as a fake for flags, nouts for arcs, 1 as endmarker */
        }

        int arcIndex = 0;
        CnfaBuilder cnfaBuilder = new CnfaBuilder(stateCount, narcs, pre.no, post.no, bos, eos, cm.maxcolor() + 1, false);
        for (State s = states; s != null; s = s.next) {
            assert s.no < stateCount;
            cnfaBuilder.setState(s.no, arcIndex);
            /* clear and skip flags "arc", by preparing to set arc at index 1 */
            arcIndex++;
            long arcValue;
            int first = arcIndex;
            for (a = s.outs; a != null; a = a.outchain) {
                switch (a.type) {
                case Compiler.PLAIN:
                    arcValue = Cnfa.packCarc(a.co, a.to.no);
                    break;
                case Compiler.LACON:
                    assert s.no != cnfaBuilder.pre;
                    arcValue = Cnfa.packCarc((short)(cnfaBuilder.ncolors + a.co), a.to.no);
                    cnfaBuilder.hasLacons = true;
                    break;
                default:
                    throw new RuntimeException("Impossible arc");
                }
                cnfaBuilder.setArc(arcIndex, arcValue);
                arcIndex++;
            }
            cnfaBuilder.carcsort(first, arcIndex - 1);
            cnfaBuilder.setArc(arcIndex++, Cnfa.packCarc(Constants.COLORLESS, 0));
        }

        assert arcIndex == narcs;
        assert cnfaBuilder.states.length != 0;

    /* mark no-progress states */
        for (a = pre.outs; a != null; a = a.outchain) {
            int ax = cnfaBuilder.states[a.to.no];
            // replace color in this arc with '1'.
            long newArcValue = Cnfa.packCarc((short)1, Cnfa.carcTarget(cnfaBuilder.arcs[ax]));
            cnfaBuilder.arcs[ax] = newArcValue;
        }
        arcIndex = cnfaBuilder.states[pre.no];
        long newArcValue = Cnfa.packCarc((short)1, Cnfa.carcTarget(cnfaBuilder.arcs[pre.no]));
        cnfaBuilder.arcs[arcIndex] = newArcValue;

        return cnfaBuilder.build();
    }
}


package com.buschmais.jqassistant.commandline.task;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.*;

import com.buschmais.jqassistant.commandline.CliConfigurationException;
import com.buschmais.jqassistant.commandline.CliExecutionException;
import com.buschmais.jqassistant.commandline.CliRuleViolationException;
import com.buschmais.jqassistant.core.analysis.api.Analyzer;
import com.buschmais.jqassistant.core.analysis.api.AnalyzerConfiguration;
import com.buschmais.jqassistant.core.analysis.api.RuleInterpreterPlugin;
import com.buschmais.jqassistant.core.analysis.api.rule.RuleException;
import com.buschmais.jqassistant.core.analysis.api.rule.RuleSet;
import com.buschmais.jqassistant.core.analysis.api.rule.Severity;
import com.buschmais.jqassistant.core.analysis.impl.AnalyzerImpl;
import com.buschmais.jqassistant.core.plugin.api.PluginRepositoryException;
import com.buschmais.jqassistant.core.plugin.api.ReportPluginRepository;
import com.buschmais.jqassistant.core.report.api.ReportContext;
import com.buschmais.jqassistant.core.report.api.ReportHelper;
import com.buschmais.jqassistant.core.report.api.ReportPlugin;
import com.buschmais.jqassistant.core.report.impl.CompositeReportPlugin;
import com.buschmais.jqassistant.core.report.impl.InMemoryReportPlugin;
import com.buschmais.jqassistant.core.report.impl.ReportContextImpl;
import com.buschmais.jqassistant.core.rule.api.reader.RuleConfiguration;
import com.buschmais.jqassistant.core.store.api.Store;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author jn4, Kontext E GmbH, 24.01.14
 */
public class AnalyzeTask extends AbstractAnalyzeTask {

    private static final String CMDLINE_OPTION_FAIL_ON_SEVERITY = "failOnSeverity";
    private static final String CMDLINE_OPTION_WARN_ON_SEVERITY = "warnOnSeverity";
    private static final String CMDLINE_OPTION_RULEPARAMETERS = "ruleParameters";
    private static final String CMDLINE_OPTION_EXECUTEAPPLIEDCONCEPTS = "executeAppliedConcepts";

    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractAnalyzeTask.class);

    private File ruleParametersFile;
    private File outputDirectory;
    private Severity failOnSeverity;
    private Severity warnOnSeverity;
    private boolean executeAppliedConcepts;

    @Override
    protected void executeTask(final Store store) throws CliExecutionException {
        LOGGER.info("Will warn on violations starting form severity '" + warnOnSeverity + "'");
        LOGGER.info("Will fail on violations starting from severity '" + failOnSeverity + "'.");
        LOGGER.info("Executing analysis.");

        ReportContext reportContext = new ReportContextImpl(outputDirectory, outputDirectory);
        Map<String, ReportPlugin> reportPlugins = getReportPlugins(reportContext);
        InMemoryReportPlugin inMemoryReportPlugin = new InMemoryReportPlugin(new CompositeReportPlugin(reportPlugins));
        AnalyzerConfiguration configuration = new AnalyzerConfiguration();
        configuration.setExecuteAppliedConcepts(executeAppliedConcepts);
        Map<String, String> ruleParameters = getRuleParameters();
        try {
            Analyzer analyzer = new AnalyzerImpl(configuration, store, getRuleInterpreterPlugins(), inMemoryReportPlugin, LOGGER);
            RuleSet availableRules = getAvailableRules();
            analyzer.execute(availableRules, getRuleSelection(availableRules), ruleParameters);
        } catch (RuleException e) {
            throw new CliExecutionException("Analysis failed.", e);
        }
        store.beginTransaction();
        LOGGER.info("Verifying results: failOnSeverity=" + failOnSeverity + ", warnOnSeverity=" + warnOnSeverity);
        try {
            final ReportHelper reportHelper = new ReportHelper(LOGGER);
            final int conceptViolations = reportHelper.verifyConceptResults(warnOnSeverity, failOnSeverity, inMemoryReportPlugin);
            final int constraintViolations = reportHelper.verifyConstraintResults(warnOnSeverity, failOnSeverity, inMemoryReportPlugin);
            if (conceptViolations > 0 || constraintViolations > 0) {
                throw new CliRuleViolationException("Failed rules detected: " + conceptViolations + " concepts, " + constraintViolations + " constraints");
            }
        } finally {
            store.commitTransaction();
        }
    }

    /**
     * Reads the given rule parameters file.
     *
     * @return The map containing the rule parameters.
     * @throws CliExecutionException
     *             If the file cannot be read.
     */
    private Map<String, String> getRuleParameters() throws CliExecutionException {
        Map<String, String> ruleParameters;
        if (ruleParametersFile == null) {
            ruleParameters = Collections.emptyMap();
        } else {
            Properties properties = new Properties();
            try {
                properties.load(new FileInputStream(ruleParametersFile));
            } catch (IOException e) {
                throw new CliExecutionException("Cannot read rule parameters file '" + ruleParametersFile.getPath() + "'.");
            }
            ruleParameters = new TreeMap<>();
            for (String name : properties.stringPropertyNames()) {
                ruleParameters.put(name, properties.getProperty(name));
            }
        }
        return ruleParameters;
    }

    /**
     * Get all configured rule interpreter plugins.
     *
     * @return The map of rule interpreter plugins grouped by their supported
     *         languages.
     * @throws CliExecutionException
     *             If the plugins cannot be loaded or configured.
     */
    private Map<String, Collection<RuleInterpreterPlugin>> getRuleInterpreterPlugins() throws CliExecutionException {
        try {
            return pluginRepository.getRuleInterpreterPluginRepository().getRuleInterpreterPlugins(Collections.<String, Object> emptyMap());
        } catch (PluginRepositoryException e) {
            throw new CliExecutionException("Cannot get report plugins.", e);
        }
    }

    /**
     * Get all configured report plugins.
     *
     * @param reportContext
     *            The ReportContext.
     * @return The list of report plugins.
     * @throws CliExecutionException
     *             If the plugins cannot be loaded or configured.
     * @param reportContext
     */
    private Map<String, ReportPlugin> getReportPlugins(ReportContext reportContext) throws CliExecutionException {
        ReportPluginRepository reportPluginRepository;
        try {
            reportPluginRepository = pluginRepository.getReportPluginRepository();
            return reportPluginRepository.getReportPlugins(reportContext, pluginProperties);
        } catch (PluginRepositoryException e) {
            throw new CliExecutionException("Cannot get report plugins.", e);
        }
    }

    @Override
    public void withOptions(final CommandLine options) throws CliConfigurationException {
        super.withOptions(options);
        String ruleParametersFileName = getOptionValue(options, CMDLINE_OPTION_RULEPARAMETERS, null);
        if (ruleParametersFileName != null) {
            this.ruleParametersFile = new File(ruleParametersFileName);
            if (!this.ruleParametersFile.exists()) {
                throw new CliConfigurationException("Cannot find rule parameters file '" + ruleParametersFileName + "'.");
            }
        } else {
            this.ruleParametersFile = null;
        }
        String reportDirectoryValue = getOptionValue(options, CMDLINE_OPTION_REPORTDIR, DEFAULT_REPORT_DIRECTORY);
        outputDirectory = new File(reportDirectoryValue);
        outputDirectory.mkdirs();
        failOnSeverity = getSeverity(
                getOptionValue(options, CMDLINE_OPTION_FAIL_ON_SEVERITY, RuleConfiguration.DEFAULT.getDefaultConstraintSeverity().getValue()));
        warnOnSeverity = getSeverity(
                getOptionValue(options, CMDLINE_OPTION_WARN_ON_SEVERITY, RuleConfiguration.DEFAULT.getDefaultConceptSeverity().getValue()));
        executeAppliedConcepts = options.hasOption(CMDLINE_OPTION_EXECUTEAPPLIEDCONCEPTS);
    }

    @Override
    public void addTaskOptions(final List<Option> options) {
        super.addTaskOptions(options);
        options.add(OptionBuilder.withArgName(CMDLINE_OPTION_RULEPARAMETERS).withDescription("The name of a properties file providing rule parameters.")
                .hasArgs().create(CMDLINE_OPTION_RULEPARAMETERS));
        options.add(OptionBuilder.withArgName(CMDLINE_OPTION_REPORTDIR).withDescription("The directory for writing reports.").hasArgs()
                .create(CMDLINE_OPTION_REPORTDIR));
        options.add(OptionBuilder.withArgName(CMDLINE_OPTION_FAIL_ON_SEVERITY)
                .withDescription("The severity threshold to fail on rule violations, i.e. to exit with an error code.").hasArgs()
                .create(CMDLINE_OPTION_FAIL_ON_SEVERITY));
        options.add(OptionBuilder.withArgName(CMDLINE_OPTION_WARN_ON_SEVERITY).withDescription("The severity threshold to warn on rule violations.").hasArgs()
                .create(CMDLINE_OPTION_WARN_ON_SEVERITY));
        options.add(OptionBuilder.withArgName(CMDLINE_OPTION_EXECUTEAPPLIEDCONCEPTS)
                .withDescription("If set also execute concepts which have already been applied.").create(CMDLINE_OPTION_EXECUTEAPPLIEDCONCEPTS));
    }
}


package com.buschmais.jqassistant.commandline.task;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;

import com.buschmais.jqassistant.commandline.CliConfigurationException;
import com.buschmais.jqassistant.commandline.CliExecutionException;
import com.buschmais.jqassistant.commandline.Task;
import com.buschmais.jqassistant.core.analysis.api.rule.*;
import com.buschmais.jqassistant.core.rule.api.reader.RuleConfiguration;
import com.buschmais.jqassistant.core.rule.api.reader.RuleParserPlugin;
import com.buschmais.jqassistant.core.rule.api.source.FileRuleSource;
import com.buschmais.jqassistant.core.rule.api.source.RuleSource;
import com.buschmais.jqassistant.core.rule.api.source.UrlRuleSource;
import com.buschmais.jqassistant.core.rule.impl.reader.RuleParser;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Abstract base class for all tasks working with rules.
 */
public abstract class AbstractAnalyzeTask extends AbstractStoreTask {

    private static final String CMDLINE_OPTION_R = "r";
    private static final String CMDLINE_OPTION_RULEDIRECTORY = "ruleDirectory";
    private static final String CMDLINE_OPTION_RULESURL = "rulesUrl";
    private static final String CMDLINE_OPTION_GROUPS = "groups";
    private static final String CMDLINE_OPTION_CONSTRAINTS = "constraints";
    private static final String CMDLINE_OPTION_CONCEPTS = "concepts";
    private static final String CMDLINE_OPTION_DEFAULT_GROUP_SEVERITY = "defaultGroupSeverity";
    private static final String CMDLINE_OPTION_DEFAULT_CONCEPT_SEVERITY = "defaultConceptSeverity";
    private static final String CMDLINE_OPTION_DEFAULT_CONSTRAINT_SEVERITY = "defaultConstraintSeverity";

    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractAnalyzeTask.class);

    private RuleConfiguration ruleConfiguration;
    private URL rulesUrl;
    private String ruleDirectory;
    private List<String> conceptIds;
    private List<String> constraintIds;
    private List<String> groupIds;

    @Override
    protected boolean isConnectorRequired() {
        return false;
    }

    protected RuleSet getAvailableRules() throws CliExecutionException {
        List<RuleSource> sources = new ArrayList<>();
        if (rulesUrl != null) {
            sources.add(new UrlRuleSource(rulesUrl));
        } else {
            File selectedDirectory = new File(ruleDirectory);
            // read rules from rules directory
            sources.addAll(readRulesDirectory(selectedDirectory));
            List<RuleSource> ruleSources = pluginRepository.getRulePluginRepository().getRuleSources();
            sources.addAll(ruleSources);
        }
        Collection<RuleParserPlugin> ruleParserPlugins;
        try {
            ruleParserPlugins = pluginRepository.getRuleParserPluginRepository().getRuleParserPlugins(ruleConfiguration);
        } catch (RuleException e) {
            throw new CliExecutionException("Cannot get rule source reader plugins.", e);
        }
        try {
            RuleParser ruleParser = new RuleParser(ruleParserPlugins);
            return ruleParser.parse(sources);
        } catch (RuleException e) {
            throw new CliExecutionException("Cannot read rules.", e);
        }
    }

    /**
     * Return the selection of rules.
     *
     * @param ruleSet
     *            The rule set.
     * @return The selection of rules.
     */
    protected RuleSelection getRuleSelection(RuleSet ruleSet) {
        return RuleSelection.select(ruleSet, groupIds,constraintIds, conceptIds);
    }

    private List<RuleSource> readRulesDirectory(File rulesDirectory) throws CliExecutionException {
        if (rulesDirectory.exists() && !rulesDirectory.isDirectory()) {
            throw new RuntimeException(rulesDirectory.getAbsolutePath() + " does not exist or is not a directory.");
        }
        LOGGER.info("Reading rules from directory " + rulesDirectory.getAbsolutePath());
        try {
            return FileRuleSource.getRuleSources(rulesDirectory);
        } catch (IOException e) {
            throw new CliExecutionException("Cannot read rules directory: " + rulesDirectory.getAbsolutePath(), e);
        }
    }

    @Override
    public void withOptions(CommandLine options) throws CliConfigurationException {
        String rulesUrl = getOptionValue(options, CMDLINE_OPTION_RULESURL, null);
        if (rulesUrl != null) {
            try {
                this.rulesUrl = new URL(rulesUrl);
            } catch (MalformedURLException e) {
                throw new CliConfigurationException("'" + rulesUrl + "' is not a valid URL.", e);
            }
        }
        ruleDirectory = getOptionValue(options, CMDLINE_OPTION_R, Task.DEFAULT_RULE_DIRECTORY);
        groupIds = getOptionValues(options, CMDLINE_OPTION_GROUPS, Collections.<String> emptyList());
        constraintIds = getOptionValues(options, CMDLINE_OPTION_CONSTRAINTS, Collections.<String> emptyList());
        conceptIds = getOptionValues(options, CMDLINE_OPTION_CONCEPTS, Collections.<String> emptyList());
        RuleConfiguration.RuleConfigurationBuilder ruleConfigurationBuilder = RuleConfiguration.builder();
        String defaultGroupSeverityValue = getOptionValue(options, CMDLINE_OPTION_DEFAULT_GROUP_SEVERITY);
        if (defaultGroupSeverityValue != null) {
            ruleConfigurationBuilder.defaultGroupSeverity(getSeverity(defaultGroupSeverityValue));
        }
        String defaultConceptSeverityValue = getOptionValue(options, CMDLINE_OPTION_DEFAULT_CONCEPT_SEVERITY);
        if (defaultConceptSeverityValue != null) {
            ruleConfigurationBuilder.defaultConceptSeverity(getSeverity(defaultConceptSeverityValue));
        }
        String defaultConstraintSeverityValue = getOptionValue(options, CMDLINE_OPTION_DEFAULT_CONSTRAINT_SEVERITY);
        if (defaultConstraintSeverityValue != null) {
            ruleConfigurationBuilder.defaultConstraintSeverity(getSeverity(defaultConstraintSeverityValue));
        }
        ruleConfiguration = ruleConfigurationBuilder.build();
    }

    @Override
    public void addTaskOptions(List<Option> options) {
        options.add(OptionBuilder.withArgName(CMDLINE_OPTION_R).withLongOpt(CMDLINE_OPTION_RULEDIRECTORY).withDescription("The directory containing rules.")
                .hasArgs().create(CMDLINE_OPTION_R));
        options.add(OptionBuilder.withArgName(CMDLINE_OPTION_RULESURL).withDescription("The URL of a file containing rules.").hasArgs()
                .create(CMDLINE_OPTION_RULESURL));
        options.add(OptionBuilder.withArgName(CMDLINE_OPTION_GROUPS).withDescription("The groups to execute (default='default').").withValueSeparator(',')
                .hasArgs().create(CMDLINE_OPTION_GROUPS));
        options.add(OptionBuilder.withArgName(CMDLINE_OPTION_CONSTRAINTS).withDescription("The constraints to verify.").withValueSeparator(',').hasArgs()
                .create(CMDLINE_OPTION_CONSTRAINTS));
        options.add(OptionBuilder.withArgName(CMDLINE_OPTION_CONCEPTS).withDescription("The concepts to apply.").withValueSeparator(',').hasArgs()
                .create(CMDLINE_OPTION_CONCEPTS));
        options.add(OptionBuilder.withArgName(CMDLINE_OPTION_DEFAULT_GROUP_SEVERITY).withDescription("The default severity for groups.").withValueSeparator(',')
                .hasArgs().create(CMDLINE_OPTION_DEFAULT_GROUP_SEVERITY));
        options.add(OptionBuilder.withArgName(CMDLINE_OPTION_DEFAULT_CONCEPT_SEVERITY).withDescription("The default severity for concepts.")
                .withValueSeparator(',').hasArgs().create(CMDLINE_OPTION_DEFAULT_CONCEPT_SEVERITY));
        options.add(OptionBuilder.withArgName(CMDLINE_OPTION_DEFAULT_CONSTRAINT_SEVERITY).withDescription("The default severity for constraints.")
                .withValueSeparator(',').hasArgs().create(CMDLINE_OPTION_DEFAULT_CONSTRAINT_SEVERITY));
    }

    protected Severity getSeverity(String severityValue) throws CliConfigurationException {
        try {
            return Severity.fromValue(severityValue);
        } catch (RuleException e) {
            throw new CliConfigurationException("Unknown severity value " + severityValue);
        }
    }
}


package com.buschmais.jqassistant.commandline;

import java.io.*;
import java.net.URL;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;

import com.buschmais.jqassistant.commandline.task.DefaultTaskFactoryImpl;
import com.buschmais.jqassistant.core.plugin.api.PluginConfigurationReader;
import com.buschmais.jqassistant.core.plugin.api.PluginRepository;
import com.buschmais.jqassistant.core.plugin.api.PluginRepositoryException;
import com.buschmais.jqassistant.core.plugin.impl.PluginConfigurationReaderImpl;
import com.buschmais.jqassistant.core.plugin.impl.PluginRepositoryImpl;

import org.apache.commons.cli.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The main class, i.e. the entry point for the CLI.
 *
 * @author jn4, Kontext E GmbH, 23.01.14
 * @author Dirk Mahler
 */
public class Main {
    private static final Logger LOGGER = LoggerFactory.getLogger(Main.class);

    private static final String ENV_JQASSISTANT_HOME = "JQASSISTANT_HOME";
    private static final String DIRECTORY_PLUGINS = "plugins";

    private static final String OPTION_HELP = "-help";

    private final TaskFactory taskFactory;

    /**
     * The main method.
     *
     * @param args
     *            The command line arguments.
     * @throws IOException
     *             If an error occurs.
     */
    public static void main(String[] args) {
        try {
            DefaultTaskFactoryImpl taskFactory = new DefaultTaskFactoryImpl();
            new Main(taskFactory).run(args);
        } catch (CliExecutionException e) {
            String message = getErrorMessage(e);
            LOGGER.error(message);
            System.exit(e.getExitCode());
        }
    }

    /**
     * Constructor.
     *
     * @param taskFactory
     *            The task factory to use.
     */
    public Main(TaskFactory taskFactory) {
        this.taskFactory = taskFactory;
    }

    /**
     * Run tasks according to the given arguments.
     *
     * @param args
     *            The arguments.
     * @throws CliExecutionException
     *             If execution fails.
     */
    public void run(String[] args) throws CliExecutionException {
        Options options = gatherOptions(taskFactory);
        CommandLine commandLine = getCommandLine(args, options);
        interpretCommandLine(commandLine, options, taskFactory);
    }

    /**
     * Extract an error message from the given exception and its causes.
     *
     * @param e
     *            The exception.
     * @return The error message.
     */
    private static String getErrorMessage(CliExecutionException e) {
        StringBuffer messageBuilder = new StringBuffer();
        Throwable current = e;
        do {
            messageBuilder.append("-> ");
            messageBuilder.append(current.getMessage());
            current = current.getCause();
        }
        while (current != null);
        return messageBuilder.toString();
    }

    /**
     * Initialize the plugin repository.
     *
     * @return The repository.
     * @throws CliExecutionException
     *             If initialization fails.
     */
    private PluginRepository getPluginRepository() throws CliExecutionException {
        PluginConfigurationReader pluginConfigurationReader = new PluginConfigurationReaderImpl(createPluginClassLoader());
        return new PluginRepositoryImpl(pluginConfigurationReader);
    }

    /**
     * Gather all options which are supported by the task (i.e. including standard and specific options).
     *
     * @return The options.
     */
    private Options gatherOptions(TaskFactory taskFactory) {
        final Options options = new Options();
        gatherTasksOptions(taskFactory, options);
        gatherStandardOptions(options);
        return options;
    }

    /**
     * Gathers the standard options shared by all tasks.
     *
     * @param options
     *            The standard options.
     */
    @SuppressWarnings("static-access")
    private void gatherStandardOptions(final Options options) {
        options.addOption(OptionBuilder.withArgName("p").withDescription(
                "Path to property file; default is jqassistant.properties in the class path").withLongOpt("properties").hasArg().create("p"));
        options.addOption(new Option("help", "print this message"));
    }

    /**
     * Gathers the task specific options for all tasks.
     *
     * @param options
     *            The task specific options.
     */
    private void gatherTasksOptions(TaskFactory taskFactory, Options options) {
        for (Task task : taskFactory.getTasks()) {
            for (Option option : task.getOptions()) {
                options.addOption(option);
            }
        }
    }

    /**
     * Returns a string containing the names of all supported tasks.
     *
     * @return The names of all supported tasks.
     */
    private String gatherTaskNames(TaskFactory taskFactory) {
        final StringBuilder builder = new StringBuilder();
        for (String taskName : taskFactory.getTaskNames()) {
            builder.append("'").append(taskName).append("' ");
        }
        return builder.toString().trim();
    }

    /**
     * Parse the command line and execute the requested task.
     *
     * @param commandLine
     *            The command line.
     * @param options
     *            The known options.
     * @throws CliExecutionException
     *             If an error occurs.
     */
    private void interpretCommandLine(CommandLine commandLine, Options options, TaskFactory taskFactory) throws CliExecutionException {
        if(commandLine.hasOption(OPTION_HELP)) {
            printUsage(options, null);
            System.exit(1);
        }

        List<String> taskNames = commandLine.getArgList();
        if (taskNames.isEmpty()) {
            printUsage(options, "A task must be specified, i.e. one  of " + gatherTaskNames(taskFactory));
            System.exit(1);
        }
        List<Task> tasks = new ArrayList<>();
        for (String taskName : taskNames) {
            Task task = taskFactory.fromName(taskName);
            if (task == null) {
                printUsage(options, "Unknown task " + taskName);
            }
            tasks.add(task);
        }
        Map<String, Object> properties = readProperties(commandLine);
        PluginRepository pluginRepository = getPluginRepository();
        try {
            executeTasks(tasks, options, commandLine, pluginRepository, properties);
        } catch (PluginRepositoryException e) {
            throw new CliExecutionException("Unexpected plugin repository problem while executing tasks.", e);
        }
    }

    private void executeTasks(List<Task> tasks, Options options, CommandLine commandLine, PluginRepository pluginRepository, Map<String, Object> properties)
            throws PluginRepositoryException, CliExecutionException {
        try {
            pluginRepository.initialize();
            for (Task task : tasks) {
                executeTask(task, options, commandLine, pluginRepository, properties);
            }
        } finally {
            pluginRepository.destroy();
        }
    }

    /**
     * Parse the command line
     *
     * @param args
     *            The arguments.
     * @param options
     *            The known options.
     * @return The command line.
     */
    private CommandLine getCommandLine(String[] args, Options options) {
        final CommandLineParser parser = new BasicParser();
        CommandLine commandLine = null;
        try {
            commandLine = parser.parse(options, args);
        } catch (ParseException e) {
            printUsage(options, e.getMessage());
            System.exit(1);
        }
        return commandLine;
    }

    /**
     * Executes a task.
     *
     * @param task
     *            The task.
     * @param option
     *            The option.
     * @param commandLine
     *            The command line.
     * @param properties
     *            The plugin properties
     * @throws IOException
     */
    private void executeTask(Task task, Options option, CommandLine commandLine, PluginRepository pluginRepository,
            Map<String, Object> properties) throws CliExecutionException {
        try {
            task.withStandardOptions(commandLine);
            task.withOptions(commandLine);
        } catch (CliConfigurationException e) {
            printUsage(option, e.getMessage());
            System.exit(1);
        }
        task.initialize(pluginRepository, properties);
        task.run();
    }

    /**
     * Read the plugin properties file if specified on the command line or if it exists on the class path.
     *
     * @param commandLine
     *            The command line.
     * @return The plugin properties.
     * @throws CliConfigurationException
     *             If an error occurs.
     */
    private Map<String, Object> readProperties(CommandLine commandLine) throws CliConfigurationException {
        final Properties properties = new Properties();
        InputStream propertiesStream;
        if (commandLine.hasOption("p")) {
            File propertyFile = new File(commandLine.getOptionValue("p"));
            if (!propertyFile.exists()) {
                throw new CliConfigurationException("Property file given by command line does not exist: " + propertyFile.getAbsolutePath());
            }
            try {
                propertiesStream = new FileInputStream(propertyFile);
            } catch (FileNotFoundException e) {
                throw new CliConfigurationException("Cannot open property file.", e);
            }
        } else {
            propertiesStream = Main.class.getResourceAsStream("/jqassistant.properties");
        }
        Map<String, Object> result = new HashMap<>();
        if (propertiesStream != null) {
            try {
                properties.load(propertiesStream);
            } catch (IOException e) {
                throw new CliConfigurationException("Cannot load properties from file.", e);
            }
            for (String name : properties.stringPropertyNames()) {
                result.put(name, properties.getProperty(name));
            }
        }
        return result;
    }

    /**
     * Print usage information.
     *
     * @param options
     *            The known options.
     * @param errorMessage
     *            The error message to append.
     */
    private void printUsage(final Options options, final String errorMessage) {
        if(errorMessage != null) {
            System.out.println("Error: " + errorMessage);
        }
        final HelpFormatter formatter = new HelpFormatter();
        formatter.printHelp(Main.class.getCanonicalName() + " <task> [options]", options);
        System.out.println("Tasks are: " + gatherTaskNames(taskFactory));
        System.out.println("Example: " + Main.class.getCanonicalName() + " scan -f java:classpath::target/classes java:classpath::target/test-classes");
    }

    /**
     * Determine the JQASSISTANT_HOME directory.
     *
     * @return The directory or `null`.
     */
    private File getHomeDirectory() {
        String dirName = System.getenv(ENV_JQASSISTANT_HOME);
        if (dirName != null) {
            File dir = new File(dirName);
            if (dir.exists()) {
                LOGGER.debug("Using JQASSISTANT_HOME '" + dir.getAbsolutePath() + "'.");
                return dir;
            } else {
                LOGGER.warn("JQASSISTANT_HOME '" + dir.getAbsolutePath() + "' points to a non-existing directory.");
                return null;
            }
        }
        LOGGER.warn("JQASSISTANT_HOME is not set.");
        return null;
    }

    /**
     * Create the class loader to be used for detecting and loading plugins.
     *
     * @return The plugin class loader.
     * @throws com.buschmais.jqassistant.commandline.CliExecutionException
     *             If the plugins cannot be loaded.
     */
    private ClassLoader createPluginClassLoader() throws CliExecutionException {
        ClassLoader parentClassLoader = Task.class.getClassLoader();
        File homeDirectory = getHomeDirectory();
        if (homeDirectory != null) {
            File pluginDirectory = new File(homeDirectory, DIRECTORY_PLUGINS);
            if (pluginDirectory.exists()) {
                final List<URL> urls = new ArrayList<>();
                final Path pluginDirectoryPath = pluginDirectory.toPath();
                SimpleFileVisitor<Path> visitor = new SimpleFileVisitor<Path>() {

                    @Override
                    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                        if (file.toFile().getName().endsWith(".jar")) {
                            urls.add(file.toFile().toURI().toURL());
                        }
                        return FileVisitResult.CONTINUE;
                    }
                };
                try {
                    Files.walkFileTree(pluginDirectoryPath, visitor);
                } catch (IOException e) {
                    throw new CliExecutionException("Cannot read plugin directory.", e);
                }
                LOGGER.debug("Using plugin URLs: " + urls);
                return new com.buschmais.jqassistant.commandline.PluginClassLoader(urls, parentClassLoader);
            }
        }
        return parentClassLoader;
    }
}


package com.buschmais.jqassistant.commandline.task;

import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.buschmais.jqassistant.commandline.CliConfigurationException;
import com.buschmais.jqassistant.commandline.CliExecutionException;
import com.buschmais.jqassistant.core.plugin.api.PluginRepositoryException;
import com.buschmais.jqassistant.core.scanner.api.*;
import com.buschmais.jqassistant.core.scanner.impl.ScannerContextImpl;
import com.buschmais.jqassistant.core.scanner.impl.ScannerImpl;
import com.buschmais.jqassistant.core.store.api.Store;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author jn4, Kontext E GmbH, 23.01.14
 */
public class ScanTask extends AbstractStoreTask {

    public static final String CMDLINE_OPTION_FILES = "f";
    public static final String CMD_LONG_OPTION_FILES = "files";
    public static final String CMDLINE_OPTION_URLS = "u";
    public static final String CMDLINE_LONG_OPTION_URLS = "urls";
    public static final String CMDLINE_OPTION_RESET = "reset";
    public static final String CMDLINE_OPTION_CONTINUEONERROR = "continueOnError";

    private static final Logger LOGGER = LoggerFactory.getLogger(ScanTask.class);

    private Map<String, String> files = Collections.emptyMap();
    private Map<String, String> urls = Collections.emptyMap();
    private boolean reset = false;
    private boolean continueOnError = false;

    @Override
    protected boolean isConnectorRequired() {
        return false;
    }

    @SuppressWarnings("static-access")
    @Override
    public void addTaskOptions(final List<Option> options) {
        options.add(OptionBuilder.withArgName(CMDLINE_OPTION_FILES).withLongOpt(CMD_LONG_OPTION_FILES)
                .withDescription("The files or directories to be scanned, comma separated, each with optional scope prefix.").withValueSeparator(',').hasArgs()
                .create(CMDLINE_OPTION_FILES));
        options.add(OptionBuilder.withArgName(CMDLINE_OPTION_URLS).withLongOpt(CMDLINE_LONG_OPTION_URLS)
                .withDescription("The URIs to be scanned, comma separated, each with optional scope prefix.").withValueSeparator(',').hasArgs()
                .create(CMDLINE_OPTION_URLS));
        options.add(
                OptionBuilder.withArgName(CMDLINE_OPTION_RESET).withDescription("Reset store before scanning (default=false).").create(CMDLINE_OPTION_RESET));
        options.add(OptionBuilder.withArgName(CMDLINE_OPTION_CONTINUEONERROR).withDescription("Continue scanning if an error is encountered. (default=false).")
                .create(CMDLINE_OPTION_CONTINUEONERROR));
    }

    @Override
    protected void executeTask(Store store) throws CliExecutionException {
        ScannerContext scannerContext = new ScannerContextImpl(store);
        Map<String, ScannerPlugin<?, ?>> scannerPlugins;
        try {
            scannerPlugins = pluginRepository.getScannerPluginRepository().getScannerPlugins(scannerContext, pluginProperties);
        } catch (PluginRepositoryException e) {
            throw new CliExecutionException("Cannot get scanner plugins.", e);
        }
        if (reset) {
            store.reset();
        }
        for (Map.Entry<String, String> entry : files.entrySet()) {
            String fileName = entry.getKey();
            String scopeName = entry.getValue();
            final File file = new File(fileName);
            String absolutePath = file.getAbsolutePath();
            if (!file.exists()) {
                LOGGER.info(absolutePath + "' does not exist, skipping scan.");
            } else {
                scan(scannerContext, file, file.getAbsolutePath(), scopeName, scannerPlugins);
            }
        }
        for (Map.Entry<String, String> entry : urls.entrySet()) {
            String uri = entry.getKey();
            String scopeName = entry.getValue();
            try {
                scan(scannerContext, new URI(uri), uri, scopeName, scannerPlugins);
            } catch (URISyntaxException e) {
                throw new CliConfigurationException("Cannot parse URI " + uri, e);
            }
        }
    }

    /**
     * Parses the given list of option values into a map of resources and their
     * associated (optional) scopes.
     *
     * Example: `maven:repository::http://my-host/repo` will be an entry with key
     * `maven:repository` and value `http://my-host/repo`.
     *
     * @param optionValues
     *            The value.
     * @return The map of resources and scopes.
     */
    private Map<String, String> parseResources(List<String> optionValues) {
        Map<String, String> resources = new HashMap<>();
        for (String file : optionValues) {
            String[] parts = file.split("::");
            String fileName;
            String scopeName = null;
            if (parts.length == 2) {
                scopeName = parts[0];
                fileName = parts[1];
            } else {
                fileName = parts[0];
            }
            resources.put(fileName, scopeName);
        }
        return resources;
    }

    private <T> void scan(ScannerContext scannerContext, T element, String path, String scopeName, Map<String, ScannerPlugin<?, ?>> scannerPlugins) {
        ScannerConfiguration configuration = new ScannerConfiguration();
        configuration.setContinueOnError(continueOnError);
        Scanner scanner = new ScannerImpl(configuration, scannerContext, scannerPlugins, pluginRepository.getScopePluginRepository().getScopes());
        Scope scope = scanner.resolveScope(scopeName);
        scanner.scan(element, path, scope);
    }

    @Override
    public void withOptions(final CommandLine options) throws CliConfigurationException {
        files = parseResources(getOptionValues(options, CMDLINE_OPTION_FILES, Collections.<String> emptyList()));
        urls = parseResources(getOptionValues(options, CMDLINE_OPTION_URLS, Collections.<String> emptyList()));
        if (files.isEmpty() && urls.isEmpty()) {
            throw new CliConfigurationException("No files, directories or urls given.");
        }
        reset = options.hasOption(CMDLINE_OPTION_RESET);
        continueOnError = options.hasOption(CMDLINE_OPTION_CONTINUEONERROR);
    }
}


package de.westnordost.countryboundaries;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;

import org.json.JSONException;
import org.json.JSONStringer;

import java.util.List;
import java.util.Map;

public class GeoJsonWriter
{
	private static final String
			TYPE = "type",
			FEATURES = "features",
			COORDINATES = "coordinates",
			GEOMETRIES = "geometries",
			GEOMETRY = "geometry",
			PROPERTIES = "properties";

	/**
	 * Writes a geometry to GeoJSON.
	 * Note that any data stored in getUserData() of the geometries that is not on the top level
	 * GeometryCollection, is lost in the process.
	 */
	public String write(Geometry geometry)
	{
		try
		{
			JSONStringer b = new JSONStringer();
			if (geometry.getClass().equals(GeometryCollection.class))
			{
				writeFeatureCollection(b, (GeometryCollection) geometry);
			}
			else
			{
				writeFeature(b, geometry);
			}
			return b.toString();
		}
		catch (JSONException e)
		{
			throw new GeoJsonException(e);
		}
	}

	private void writeFeatureCollection(JSONStringer b, GeometryCollection g) throws JSONException
	{
		b.object();
		b.key(TYPE).value("FeatureCollection");
		b.key(FEATURES).array();
		for (int i = 0; i < g.getNumGeometries(); i++)
		{
			writeFeature(b, g.getGeometryN(i));
		}
		b.endArray();
		b.endObject();
	}

	private void writeFeature(JSONStringer b, Geometry g) throws JSONException
	{
		b.object();
		b.key(TYPE).value("Feature");
		b.key(PROPERTIES);
		writeProperties(b, g.getUserData());
		b.key(GEOMETRY);
		writeGeometry(b, g);
		b.endObject();
	}

	private void writeProperties(JSONStringer b, Object object) throws JSONException
	{
		if (object == null || !(object instanceof Map))
		{
			b.object().endObject();
		}
		else
		{
			writeObject(b, object);
		}
	}

	private void writeObject(JSONStringer b, Object object) throws JSONException
	{
		if (object == null)
		{
			b.value(null);
		}
		else if (object instanceof Map)
		{
			b.object();
			Map props = (Map) object;
			for (Object o : props.entrySet())
			{
				Map.Entry entry = (Map.Entry) o;
				b.key(String.valueOf(entry.getKey()));
				writeObject(b, entry.getValue());
			}
			b.endObject();
		}
		else if (object instanceof List)
		{
			b.array();
			List list = (List) object;
			for (Object o : list)
			{
				writeObject(b, o);
			}
			b.endArray();
		}
		else
		{
			b.value(object);
		}
	}

	private void writeGeometry(JSONStringer b, Geometry g) throws JSONException
	{
		b.object();

		if (g instanceof Point)
		{
			b.key(TYPE).value("Point").key(COORDINATES);
			writeCoordinate(b, g.getCoordinate());
		}
		else if (g instanceof MultiPoint)
		{
			b.key(TYPE).value("MultiPoint").key(COORDINATES);
			writeCoordinates(b, g.getCoordinates());
		}
		else if (g instanceof LineString)
		{
			b.key(TYPE).value("LineString").key(COORDINATES);
			writeCoordinates(b, g.getCoordinates());
		}
		else if (g instanceof MultiLineString)
		{
			b.key(TYPE).value("MultiLineString").key(COORDINATES);
			writeMultiCoordinates(b, g);
		}
		else if (g instanceof Polygon)
		{
			b.key(TYPE).value("Polygon").key(COORDINATES);
			writePolygon(b, createReversed((Polygon) g));
		}
		else if (g instanceof MultiPolygon)
		{
			b.key(TYPE).value("MultiPolygon").key(COORDINATES);
			MultiPolygon mp = (MultiPolygon) g;
			Polygon[] reversedPolys = new Polygon[mp.getNumGeometries()];
			for (int i = 0; i < mp.getNumGeometries(); i++)
			{
				reversedPolys[i] = createReversed((Polygon) mp.getGeometryN(i));
			}
			mp = mp.getFactory().createMultiPolygon(reversedPolys);

			writeMultiPolygon(b, mp);
		}
		else if (g instanceof GeometryCollection)
		{
			b.key(TYPE).value("GeometryCollection").key(GEOMETRIES);
			b.array();
			for (int i = 0; i < g.getNumGeometries(); i++)
			{
				writeGeometry(b, g.getGeometryN(i));
			}
			b.endArray();
		}
		else
		{
			throw new GeoJsonException(
					"Converting geometry " + g.getClass().getSimpleName() + " to GeoJSON is not supported");
		}

		b.endObject();
	}

	/* The shell of a polygon in GeoJSON is defined in counterclockwise order, in JTS, it is the
	   other way round */
	private static Polygon createReversed(Polygon geometry)
	{
		Polygon r = (Polygon) geometry.clone();
		r.normalize();
		return (Polygon) r.reverse();
	}

	private void writeMultiCoordinates(JSONStringer b, Geometry g)
	{
		b.array();
		for (int i = 0; i < g.getNumGeometries(); i++)
		{
			writeCoordinates(b, g.getGeometryN(i).getCoordinates());
		}
		b.endArray();
	}

	private void writeMultiPolygon(JSONStringer b, MultiPolygon mp)
	{
		b.array();
		for (int i = 0; i < mp.getNumGeometries(); i++)
		{
			writePolygon(b, (Polygon) mp.getGeometryN(i));
		}
		b.endArray();
	}

	private void writePolygon(JSONStringer b, Polygon p)
	{
		b.array();
		writeCoordinates(b, p.getExteriorRing().getCoordinates());
		for (int i = 0; i < p.getNumInteriorRing(); i++)
		{
			writeCoordinates(b, p.getInteriorRingN(i).getCoordinates());
		}
		b.endArray();
	}

	private void writeCoordinates(JSONStringer b, Coordinate[] coords)
	{
		b.array();
		for (Coordinate coord : coords)
		{
			writeCoordinate(b, coord);
		}
		b.endArray();
	}

	private void writeCoordinate(JSONStringer b, Coordinate coord)
	{
		b.array().value(coord.x).value(coord.y);
		if (!Double.isNaN(coord.z)) b.value(coord.z);
		b.endArray();
	}
}


package de.westnordost.countryboundaries;

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class CountryBoundaries
{
	private final CountryBoundariesCell[] raster;
	private final int rasterWidth;
	private final Map<String, Double> geometrySizes;

	CountryBoundaries(
			CountryBoundariesCell[] raster, int rasterWidth, Map<String, Double> geometrySizes)
	{
		this.raster = raster;
		this.rasterWidth = rasterWidth;
		this.geometrySizes = geometrySizes;
	}

	public static CountryBoundaries load(InputStream is) throws IOException
	{
		return read(new ObjectInputStream(is));
	}

	/** @param longitude longitude of geo position (-180...180)
	 *  @param latitude latitude of geo position (-90...90)
	 *  @param ids ids of the countries to look for
	 *  @return whether the given position is in any of the countries with the given ids */
	public boolean isInAny(double longitude, double latitude, Collection<String> ids)
	{
		return getCell(longitude, latitude).isInAny(longitude, latitude, ids);
	}

	/** @param longitude longitude of geo position (-180...180)
	 *  @param latitude latitude of geo position (-90...90)
	 *  @param id ids of the country to look for
	 *  @return whether the given position is in the country with the given id */
	public boolean isIn(double longitude, double latitude, String id)
	{
		return isInAny(longitude, latitude, Collections.singleton(id));
	}

	/** @param longitude longitude of geo position (-180...180)
	 *  @param latitude latitude of geo position (-90...90)
	 *  @return the ids of the countries the given position is contained in, ordered by size ascending */
	public List<String> getIds(double longitude, double latitude)
	{
		List<String> result = getCell(longitude, latitude).getIds(longitude, latitude);
		Collections.sort(result, this::compareSize);
		return result;
	}

	/** Identify which countries are guaranteed to contain the given bounding box fully.
	 *  The given bounding box may wrap around the 180th longitude, i.e minLongitude = 170 and
	 *  maxLongitude = -170.
	 *
	 *  @param minLongitude minimum longitude of geo position (-180...180)
	 *  @param minLatitude minimum latitude of geo position (-90...90)
	 *  @param maxLongitude maximum longitude of geo position (-180...180)
	 *  @param maxLatitude maximum latitude of geo position (-90...90)
	 *
	 *  @return the ids of the countries the given bounding box is guaranteed to be contained in,
	 *          not in any particular order */
	public Set<String> getContainingIds(
			double minLongitude, double minLatitude, double maxLongitude, double maxLatitude)
	{
		Set<String> ids = new HashSet<>();
		forCellsIn(minLongitude, minLatitude, maxLongitude, maxLatitude, cell ->
		{
			if(ids.isEmpty())
			{
				ids.addAll(cell.getContainingIds());
			}
			else
			{
				ids.retainAll(cell.getContainingIds());
			}
		});
		return ids;
	}

	/** Identify which countries intersect with the given bounding box. The given bounding box may
	 *  wrap around the 180th longitude, i.e minLongitude = 170 and maxLongitude = -170.
	 *
	 *  @param minLongitude minimum longitude of geo position (-180...180)
	 *  @param minLatitude minimum latitude of geo position (-90...90)
	 *  @param maxLongitude maximum longitude of geo position (-180...180)
	 *  @param maxLatitude maximum latitude of geo position (-90...90)
	 *
	 *  @return the ids of the countries the given bounding box intersects with, not in any
	 *  particular order */
	public Set<String> getIntersectingIds(
			double minLongitude, double minLatitude, double maxLongitude, double maxLatitude)
	{
		Set<String> ids = new HashSet<>();
		forCellsIn(minLongitude, minLatitude, maxLongitude, maxLatitude, cell ->
		{
			ids.addAll(cell.getAllIds());
		});
		return ids;
	}

	private void forCellsIn(
			double minLongitude, double minLatitude, double maxLongitude, double maxLatitude,
			CellRunnable runnable)
	{
		if (minLatitude < -90 || minLatitude > 90)
			throw new IllegalArgumentException("minLatitude is out of bounds");
		if (maxLatitude < -90 || maxLatitude > 90)
			throw new IllegalArgumentException("maxLatitude is out of bounds");
		if (minLatitude > maxLatitude)
			throw new IllegalArgumentException("maxLatitude is smaller than minLatitude");

		int minX = longitudeToCellX(minLongitude);
		int maxY = latitudeToCellY(minLatitude);
		int maxX = longitudeToCellX(maxLongitude);
		int minY = latitudeToCellY(maxLatitude);

		// might wrap around
		int stepsX = minX > maxX ? rasterWidth - minX + maxX : maxX - minX;

		for (int xStep = 0; xStep <= stepsX; ++xStep)
		{
			int x = (minX + xStep) % rasterWidth;
			for (int y = minY; y <= maxY; y++)
			{
				runnable.run(raster[y * rasterWidth + x]);
			}
		}
	}

	private interface CellRunnable
	{
		void run(CountryBoundariesCell cell);
	}

	private CountryBoundariesCell getCell(double longitude, double latitude)
	{
		if (latitude < -90 || latitude > 90) throw new IllegalArgumentException("latitude is out of bounds");
		int x = longitudeToCellX(longitude);
		int y = latitudeToCellY(latitude);
		return raster[y * rasterWidth + x];
	}

	private int longitudeToCellX(double longitude)
	{
		return (int) Math.min(
				rasterWidth - 1,
				Math.floor((180 + normalize(longitude, -180, 360)) / 360.0 * rasterWidth)
		);
	}

	private int latitudeToCellY(double latitude)
	{
		int rasterHeight = raster.length / rasterWidth;
		return (int) Math.max(
				0,
				Math.ceil((90 - latitude) / 180.0 * rasterHeight) - 1
		);
	}

	private static double normalize(double value, double startAt, double base)
	{
		while (value < startAt) value += base;
		while (value > startAt + base) value -= base;
		return value;
	}

	private double getSize(String id)
	{
		Double size = geometrySizes.get(id);
		return size != null ? size : 0;
	}

	private int compareSize(String isoCode1, String isoCode2)
	{
		return (int) (getSize(isoCode1) - getSize(isoCode2));
	}

	@Override public boolean equals(Object o)
	{
		if (this == o) return true;
		if (o == null || getClass() != o.getClass()) return false;

		CountryBoundaries that = (CountryBoundaries) o;

		return
				rasterWidth == that.rasterWidth
						&& Arrays.equals(raster, that.raster)
						&& geometrySizes.equals(that.geometrySizes);
	}

	@Override public int hashCode()
	{
		return rasterWidth + 31 * (Arrays.hashCode(raster) + 31 * geometrySizes.hashCode());
	}

	void write(ObjectOutputStream out) throws IOException
	{
		out.writeInt(geometrySizes.size());
		for (Map.Entry<String, Double> e : geometrySizes.entrySet())
		{
			out.writeUTF(e.getKey());
			out.writeDouble(e.getValue());
		}
		out.writeInt(rasterWidth);
		out.writeInt(raster.length);
		for (int c = 0; c < raster.length; c++)
		{
			raster[c].write(out);
		}
	}

	static CountryBoundaries read(ObjectInputStream in) throws IOException
	{
		int geometrySizesCount = in.readInt();
		Map<String, Double> geometrySizes = new HashMap<>(geometrySizesCount);
		for (int i = 0; i < geometrySizesCount; i++)
		{
			geometrySizes.put(in.readUTF().intern(), in.readDouble());
		}
		int rasterWidth = in.readInt();
		int rasterSize = in.readInt();
		CountryBoundariesCell[] raster = new CountryBoundariesCell[rasterSize];
		for (int i= 0; i < rasterSize; i++)
		{
			raster[i] = CountryBoundariesCell.read(in);
		}
		return new CountryBoundaries(raster, rasterWidth, geometrySizes);
	}
}


package de.westnordost.countryboundaries;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Arrays;

/** Represents the areas that one country with the given id covers. */
class CountryAreas
{
	final String id;
	private final Point[][] outer;
	private final Point[][] inner;

	CountryAreas(String id, Point[][] outer, Point[][] inner)
	{
		this.id = id;
		this.outer = outer;
		this.inner = inner;
	}

	boolean covers(Point point)
	{
		int insides = 0;
		for (Point[] area : outer)
		{
			if(isPointInPolygon(point, area)) insides++;
		}
		for (Point[] area : inner)
		{
			if(isPointInPolygon(point, area)) insides--;
		}
		return insides > 0;
	}

	@Override public boolean equals(Object o)
	{
		if (this == o) return true;
		if (o == null || getClass() != o.getClass()) return false;

		CountryAreas that = (CountryAreas) o;

		return id.equals(that.id) && Arrays.deepEquals(inner, that.inner) && Arrays.deepEquals(outer, that.outer);
	}

	@Override public int hashCode()
	{
		return 31 *(31 * id.hashCode() + Arrays.deepHashCode(outer)) + Arrays.deepHashCode(inner);
	}

	@Override public String toString()
	{
		return "" + id + ":" + Arrays.deepToString(outer) + " - " + Arrays.deepToString(inner);
	}

	void write(ObjectOutputStream out) throws IOException
	{
		out.writeUTF(id);
		out.writeInt(outer.length);
		for (Point[] ring : outer)
		{
			writeRing(ring, out);
		}
		out.writeInt(inner.length);
		for (Point[] ring : inner)
		{
			writeRing(ring, out);
		}
	}

	private void writeRing(Point[] points, ObjectOutputStream out) throws IOException
	{
		out.writeInt(points.length);
		for (Point point : points)
		{
			point.write(out);
		}
	}

	static CountryAreas read(ObjectInputStream in) throws IOException
	{
		String id = in.readUTF().intern();
		Point[][] outer = new Point[in.readInt()][];
		for (int i = 0; i < outer.length; i++)
		{
			outer[i] = readRing(in);
		}
		Point[][] inner = new Point[in.readInt()][];
		for (int i = 0; i < inner.length; i++)
		{
			inner[i] = readRing(in);
		}
		return new CountryAreas(id, outer, inner);
	}

	private static Point[] readRing(ObjectInputStream in) throws IOException
	{
		Point[] ring = new Point[in.readInt()];
		for (int j = 0; j < ring.length; j++)
		{
			ring[j] = Point.read(in);
		}
		return ring;
	}

	// modified from:

	// Copyright 2000 softSurfer, 2012 Dan Sunday
	// This code may be freely used and modified for any purpose
	// providing that this copyright notice is included with it.
	// SoftSurfer makes no warranty for this code, and cannot be held
	// liable for any real or imagined damage resulting from its use.
	// Users of this code must verify correctness for their application.
	// http://geomalgorithms.com/a03-_inclusion.html

	private static boolean isPointInPolygon(Point p, Point[] v )
	{
		int wn = 0;
		for (int j = 0, i = v.length-1; j < v.length; i = j++) {
			if (v[i].y <= p.y) {
				if(v[j].y > p.y) {
					if (isLeft(v[i],v[j],p) > 0)
						++wn;
				}
			} else {
				if(v[j].y <= p.y) {
					if (isLeft(v[i],v[j],p) < 0)
						--wn;
				}
			}
		}
		return wn != 0;
	}

	private static long isLeft(Point p0, Point p1, Point p)
	{
		return ((long) p1.x - p0.x) * ((long) p.y - p0.y) - ((long) p.x - p0.x) * ((long) p1.y - p0.y);
	}
}