/**
 *  Contract
 *  Copyright 28.01.2017 by Michael Peter Christen, @0rb1t3r
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *  
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *  
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program in the file lgpl21.txt
 *  If not, see <http://www.gnu.org/licenses/>.
 */

package net.yacy.grid.contracts;

import java.util.ArrayList;
import java.util.List;

import org.json.JSONArray;
import org.json.JSONObject;

/**
 * A contract is an object which should be passed around YaCy services to trigger actions
 * in the YaCy services. The json String from a contract is the message that can be pushed to
 * a grid broker. A Contract has two main parts:
 * - a table consisting of context information of a contract. All services may enrich that data.
 * - an actions list which is a pre-defined process list for all the different stages a job has to go through
 * There is also some metadata which describes the environment of such a contract, like the number of entries
 * in the data table and a possible offset. There is also the option to set a 'query' attribute which
 * makes it possible to carry a search result within a contract.
 */
public class Contract extends JSONObject {
    
    private final static String KEY_METADATA = "metadata";
    private final static String KEY_DATA     = "data";
    private final static String KEY_ACTIONS  = "actions";
    private final static String KEY_QUERY    = "query";
    private final static String KEY_OFFSET   = "offset";
    private final static String KEY_HITS     = "hits";
    private final static String KEY_COUNT    = "count";

    public Contract() {
        super(true);
        // create base data structure
        getMetadata();
        getData();
        getActions();
    }
    
    public Contract(JSONObject json) {
        this();
        if (json.has(Contract.KEY_METADATA)) this.put(KEY_METADATA, json.getJSONObject(KEY_METADATA));
        if (json.has(Contract.KEY_DATA)) this.setData(json.getJSONArray(KEY_DATA));
        if (json.has(KEY_ACTIONS)) this.put(KEY_ACTIONS, json.getJSONArray(KEY_ACTIONS));
    }
    
    public boolean equals(Object o) {
        if (!(o instanceof Contract)) return false;
        Contract t = (Contract) o;
        return this.getData().equals(t.getData());
    }
    
    public Contract setOffset(int offset) {
        getMetadata().put(KEY_OFFSET, offset);
        return this;
    }
    
    public int getOffset() {
        return getMetadata().has(KEY_OFFSET) ? getMetadata().getInt(KEY_OFFSET) : 0;
    }
    
    public int getCount() {
        return getData().length();
    }

    public boolean isFailed() {
        return getData().length() == 0;
    }

    public boolean hasEmptyDirection(String key) {
        List<String> directions = this.getDirections(key);
        return directions.size() == 0 || directions.get(0).length() == 0;
    }
    
    public Contract setHits(int hits) {
        getMetadata().put(KEY_HITS, hits);
        return this;
    }
    
    public int getHits() {
        return getMetadata().has(KEY_HITS) ? getMetadata().getInt(KEY_HITS) : 0;
    }

    public Contract setProcess(String processName) {
        getMetadata().put("process", processName);
        return this;
    }

    public Contract setQuery(String query) {
        getMetadata().put(KEY_QUERY, query);
        return this;
    }
    
    public String getQuery() {
        return getMetadata().has(KEY_QUERY) ? getMetadata().getString(KEY_QUERY) : "";
    }
    
    private JSONObject getMetadata() {
        JSONObject md;
        if (this.has(KEY_METADATA)) md = this.getJSONObject(KEY_METADATA); else {
            md = new JSONObject();
            this.put(KEY_METADATA, md);
        }
        if (!md.has(KEY_COUNT)) md.put(KEY_COUNT, getData().length());
        return md;
    }
    
    public Contract setData(JSONArray table) {
        this.put(KEY_DATA, table);
        JSONObject md = getMetadata();
        md.put(KEY_COUNT, getData().length());
        return this;
    }
    
    public JSONArray getData() {
        if (this.has(KEY_DATA)) return this.getJSONArray(KEY_DATA);
        JSONArray a = new JSONArray();
        this.put(KEY_DATA, a);
        return a;
    }
    
    public Contract mergeData(JSONArray table1) {
        JSONArray table0 = this.getData();
        int t0c = 0;
        for (int i = 0; i < table1.length(); i++) {
            JSONObject j1i = table1.getJSONObject(i);
            while (t0c < table0.length() && anyObjectKeySame(j1i, table0.getJSONObject(t0c))) {t0c++;}
            if (t0c >= table0.length()) table0.put(new JSONObject(true));
            table0.getJSONObject(t0c).putAll(table1.getJSONObject(i));
        }
        setData(table0);
        return this;
    }
    
    private final static boolean anyObjectKeySame(final JSONObject a, final JSONObject b) {
        for (String k: a.keySet()) if (b.has(k)) return true;
        return false;
    }
    
    public Contract addDirection(String featureName, String direction) {
        JSONArray data = getData();
        
        // find first occurrence of key in rows
        int rowc = 0; boolean found = false;
        while (rowc < data.length()) {
            JSONObject row = data.getJSONObject(rowc);
            if (row.has(featureName)) found = true;
            if (found) break;
            rowc++;
        }
        if (found) {
            // insert feature in front of row
            if (rowc == 0) {
                // insert a row and shift everything up
                JSONArray newData = new JSONArray();
                JSONObject row = new JSONObject();
                row.put(featureName, direction);
                newData.put(row);
                for (Object o: data) newData.put(o);
                this.setData(newData);
            } else {
                JSONObject row = data.getJSONObject(rowc - 1);
                row.put(featureName, direction);
            }
        } else {
            // insert into first line
            if (data.length() == 0) {
                JSONObject row = new JSONObject();
                row.put(featureName, direction);
                data.put(row);
            } else {
                JSONObject row = data.getJSONObject(0);
                row.put(featureName, direction);
            }
        }
        return this;
    }
    
    public List<String> getDirections(String featureName) {
        List<String> list = new ArrayList<>();
        JSONArray table = this.getData();
        if (table != null && table.length() > 0) {
            JSONObject row = table.getJSONObject(0);
            for (String key: row.keySet()) {
                if (key.equals(featureName)) list.add(row.get(key).toString());
            }
        }
        return list;
    }
    
    public Contract addActions(List<Action> actions) {
        JSONArray a = getActionsJSON();
        actions.forEach(action -> a.put(action.toJSONClone()));
        return this;
    }
    
    public Contract addAction(Action action) {
        JSONArray a = getActionsJSON();
        a.put(action.toJSONClone());
        return this;
    }
    
    public List<Action> getActions() {
        List<Action> actions = new ArrayList<>();
        getActionsJSON().forEach(action -> actions.add(new Action((JSONObject) action)));
        return actions;
    }
    
    public Action getNextAction() {
        JSONArray a = getActionsJSON();
        if (a.length() == 0) return null;
        return new Action(a.getJSONObject(0));
    }
    
    public Action removeNextAction() {
        JSONArray a = getActionsJSON();
        if (a.length() == 0) return null;
        return new Action((JSONObject) this.getJSONArray(KEY_ACTIONS).remove(0));
    }
    
    private JSONArray getActionsJSON() {
        JSONArray actions;
        if (!this.has(KEY_ACTIONS)) {
            actions = new JSONArray();
            this.put(KEY_ACTIONS, actions);
        } else {
            actions = this.getJSONArray(KEY_ACTIONS);
        }
        return actions;
    }
    
    public JSONObject toJSON() {
        return this;
    }
    
    public static void main(String[] args) {
        Contract contract = new Contract();
        contract.setQuery("*");
        contract.addDirection("url", "http://fsfe.de");
        Action action = new Action()
                .setQuery(new JSONObject().put("q", "abc"))
                .setServletName("yacysearch");
        contract.addAction(action);
        System.out.println(contract.toString(2));
    }
    
}