/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jena.sparql.sse;

import org.apache.jena.atlas.io.IndentedLineBuffer;
import org.apache.jena.atlas.logging.Log;
import org.apache.jena.graph.Node;
import org.apache.jena.graph.NodeFactory;
import org.apache.jena.sparql.core.Var;
import org.apache.jena.sparql.util.FmtUtils;

public class Item extends ItemLocation {
    protected ItemList list = null;
    protected Node node = null;
    protected String symbol = null;
    protected boolean isNil = false;

    // Constants
    public static final Item nil = createNil();
    public static final Item defaultItem = createSymbol("_");

    public static Item createList() {
        return createList(noLine, noColumn);
    }

    public static Item createList(int line, int column) {
        Item item = new Item(line, column);
        item.list = new ItemList(line, column);
        return item;
    }

    public static Item createList(ItemList list) {
        return createList(list, noLine, noColumn);
    }

    public static Item createList(ItemList list, int line, int column) {
        Item item = new Item(line, column);
        item.list = list;
        return item;
    }

    public static Item createNode(Node node) {
        return createNode(node, noLine, noColumn);
    }

    public static Item createNode(Node node, int line, int column) {
        Item item = new Item(line, column);
        item.node = node;
        return item;
    }

    public static Item createSymbol(String symbol) {
        return createSymbol(symbol, noLine, noColumn);
    }

    public static Item createSymbol(String symbol, int line, int column) {
        if ( symbol.equals("nil") )
            return createNil(line, column);
        Item item = new Item(line, column);
        item.symbol = symbol;
        return item;
    }

    private static Item createNil() {
        return createNil(noLine, noColumn);
    }

    private static Item createNil(int line, int column) {
        // Not a symbol
        Item item = new Item(noLine, noColumn);
        item.isNil = true;
        return item;
    }

    // --- Convenience ways to make things

    public static Item createTagged(String tag) {
        Item tagged = Item.createList();
        tagged.getList().add(Item.createSymbol(tag));
        return tagged;
    }

    public static void addPair(ItemList list, String key, String value) {
        addPair(list, Item.createSymbol(key), Item.createNode(NodeFactory.createLiteralString(value)));
    }

    public static void addPair(ItemList list, String key, Node node) {
        addPair(list, Item.createSymbol(key), Item.createNode(node));
    }

    public static void addPair(ItemList list, Node key, Node value) {
        addPair(list, Item.createNode(key), Item.createNode(value));
    }

    public static void addPair(ItemList list, Item key, Item value) {
        Item pair = makePair(key, value);
        list.add(pair);
    }

    public static Item makePair(Item item1, Item item2) {
        Item list = Item.createList();
        list.getList().add(item1);
        list.getList().add(item2);
        return list;
    }

    public static Item find(ItemList list, String key) {
        for ( Item x : list ) {
            if ( x.isTagged(key) ) {
                return x;
            }
        }
        return null;
    }

    private Item(int line, int column) {
        super(line, column);
    }

    private Item() {
        super(noLine, noColumn);
    }

    // ---- Equality and hashcode

    static class ItemHashCode implements ItemVisitor {
        int hashCode = 0;
        @Override
        public void visit(Item item, ItemList list) {
            hashCode = list.hashCode();
        }

        @Override
        public void visit(Item item, Node node) {
            hashCode = node.hashCode();
        }

        @Override
        public void visit(Item item, String symbol) {
            hashCode = symbol.hashCode();
        }

        @Override
        public void visitNil(Item item) {
            hashCode = -99;
        }
    }

    @Override
    public int hashCode() {
        ItemHashCode itemHashCode = new ItemHashCode();
        this.visit(itemHashCode);
        return itemHashCode.hashCode;
    }

    static class ItemEquals implements ItemVisitor {
        private Item other;
        ItemEquals(Item other) {
            this.other = other;
        }
        boolean result = false;

        @Override
        public void visit(Item item, ItemList list) {
            result = (other.isList() && other.getList().equals(list));
        }

        @Override
        public void visit(Item item, Node node) {
            result = (other.isNode() && other.getNode().equals(node));
        }

        @Override
        public void visit(Item item, String symbol) {
            result = (other.isSymbol() && other.getSymbol().equals(symbol));
        }

        @Override
        public void visitNil(Item item) {
            result = other.isNil();
        }

    }

    @Override
    public boolean equals(Object other) {
        if ( this == other )
            return true;
        if ( !(other instanceof Item) )
            return false;

        ItemEquals x = new ItemEquals((Item)other);
        this.visit(x);
        return x.result;
    }

    public boolean sameSymbol(Item item) {
        if ( item.isSymbol() )
            return sameSymbol(item.getSymbol());
        return false;
    }

    public boolean sameSymbol(String symbolStr) {
        return isSymbol() && getSymbol().equalsIgnoreCase(symbolStr);
    }

    // ----

    public ItemList getList() {
        return list;
    }

    public Node getNode() {
        return node;
    }

    // public String getPrefixedName() { return prefixedName ; }
    public String getSymbol() {
        return symbol;
    }

    public double getDouble() {
        if ( !isNode() )
            throw new ItemException("Not a node, can't be a double: " + this);
        if ( !getNode().isLiteral() )
            throw new ItemException("Not a literal, can't be a double: " + this);
        return ((Number)(getNode().getLiteralValue())).doubleValue();
    }

    public long getInteger() {
        if ( !isNode() )
            throw new ItemException("Not a node, can't be an integer: " + this);
        if ( !getNode().isLiteral() )
            throw new ItemException("Not a literal, can't be a integer: " + this);
        return ((Number)(getNode().getLiteralValue())).longValue();
    }

    public int getInt() {
        if ( !isNode() )
            throw new ItemException("Not a node, can't be an integer: " + this);
        if ( !getNode().isLiteral() )
            throw new ItemException("Not a literal, can't be a integer: " + this);
        return ((Number)(getNode().getLiteralValue())).intValue();
    }

    public long getLong() {
        if ( !isNode() )
            throw new ItemException("Not a node, can't be an integer: " + this);
        if ( !getNode().isLiteral() )
            throw new ItemException("Not a literal, can't be a integer: " + this);
        return ((Number)(getNode().getLiteralValue())).longValue();
    }

    // Get an integer-like value, ignoring typing
    public long asLong() {
        if ( isNode() ) {
            if ( getNode().isLiteral() )
                // Ignore typing.
                return Long.parseLong(getNode().getLiteralLexicalForm());
        }
        if ( isSymbol() )
            return Integer.parseInt(getSymbol());
        throw new ItemException("Not a literal or string: " + this);
    }

    public String sniff() {
        if ( !isTaggable() )
            return null;
        return getList().get(0).getSymbol();
    }

    public boolean isTaggedIgnoreCase(String tag) {
        if ( !isTaggable() )
            return false;
        return getList().isTaggedIgnoreCase(tag);
    }

    public boolean isTagged(String tag) {
        if ( !isTaggable() )
            return false;
        return getList().isTagged(tag);
    }

    public boolean isTagged() {
        if ( !isTaggable() )
            return false;
        return list.get(0).isSymbol();
    }

    private boolean isTaggable() {
        if ( !isList() )
            return false;
        if ( list.isEmpty() )
            return false;
        return true;
    }

    public boolean isDefault() {
        return Item.defaultItem.equals(this);
    }

    public boolean isNil() {
        return isNil;
    }

    public boolean isList() {
        return list != null;
    }

    public boolean isNode() {
        return getNode() != null;
    }

    public boolean isNodeURI() {
        return isNode() && getNode().isURI();
    }

    public boolean isVar() {
        return Var.isVar(getNode());
    }

    public boolean isNodeLiteral() {
        return isNode() && getNode().isLiteral();
    }

    public boolean isNodeBNode() {
        return isNode() && getNode().isBlank();
    }

    public boolean isSymbol() {
        return symbol != null;
    }

    public boolean isSymbol(String testSymbol) {
        if ( symbol == null )
            return false;
        return symbol.equals(testSymbol);
    }

    public boolean isSymbolIgnoreCase(String testSymbol) {
        if ( symbol == null )
            return false;
        return symbol.equalsIgnoreCase(testSymbol);
    }

    public void visit(ItemVisitor visitor) {
        if ( isList() )
            visitor.visit(this, getList());
        else if ( isNode() )
            visitor.visit(this, getNode());
        else if ( isSymbol() )
            visitor.visit(this, getSymbol());
        else if ( isNil() )
            visitor.visitNil(this);
        else
            Log.error(Item.class, "broken item");
    }

    @Override
    public String toString() {
        IndentedLineBuffer iBuff = new IndentedLineBuffer();
        ItemWriter.write(iBuff, this, null);
        // iBuff.getIndentedWriter().ensureStartOfLine() ;
        // iBuff.getIndentedWriter().flush() ;
        return iBuff.asString();
    }

    /** For log messages etc. Does not guarantee round trip. */
    public String shortString() {
        if ( isSymbol() )
            return getSymbol();
        if ( isNode() )
            return FmtUtils.stringForNode(getNode());
        return getList().shortString();
    }
}
