/*
 * Copyright 2017 HugeGraph Authors
 *
 * 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 com.baidu.hugegraph.backend.store.hbase;

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

import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellScanner;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.util.Bytes;

import com.baidu.hugegraph.backend.query.Query;
import com.baidu.hugegraph.backend.serializer.BinaryBackendEntry;
import com.baidu.hugegraph.backend.serializer.BinaryEntryIterator;
import com.baidu.hugegraph.backend.serializer.BinarySerializer;
import com.baidu.hugegraph.backend.store.BackendEntry;
import com.baidu.hugegraph.backend.store.BackendEntry.BackendColumn;
import com.baidu.hugegraph.backend.store.BackendEntryIterator;
import com.baidu.hugegraph.backend.store.hbase.HbaseSessions.RowIterator;
import com.baidu.hugegraph.backend.store.hbase.HbaseSessions.Session;
import com.baidu.hugegraph.type.HugeType;
import com.baidu.hugegraph.util.NumericUtil;

public class HbaseTables {

    public static class Counters extends HbaseTable {

        private static final String TABLE = HugeType.COUNTER.string();
        private static final byte[] COL = Bytes.toBytes(TABLE);

        public Counters() {
            super(TABLE);
        }

        public long getCounter(Session session, HugeType type) {
            byte[] key = new byte[]{type.code()};
            RowIterator results = session.get(this.table(), CF, key);
            if (results.hasNext()) {
                Result row = results.next();
                return NumericUtil.bytesToLong(row.getValue(CF, COL));
            } else {
                return 0L;
            }
        }

        public void increaseCounter(Session session, HugeType type,
                                    long increment) {
            byte[] key = new byte[]{type.code()};
            session.increase(this.table(), CF, key, COL, increment);
        }
    }

    public static class VertexLabel extends HbaseTable {

        public static final String TABLE = HugeType.VERTEX_LABEL.string();

        public VertexLabel() {
            super(TABLE);
        }
    }

    public static class EdgeLabel extends HbaseTable {

        public static final String TABLE = HugeType.EDGE_LABEL.string();

        public EdgeLabel() {
            super(TABLE);
        }
    }

    public static class PropertyKey extends HbaseTable {

        public static final String TABLE = HugeType.PROPERTY_KEY.string();

        public PropertyKey() {
            super(TABLE);
        }
    }

    public static class IndexLabel extends HbaseTable {

        public static final String TABLE = HugeType.INDEX_LABEL.string();

        public IndexLabel() {
            super(TABLE);
        }
    }

    public static class Vertex extends HbaseTable {

        public static final String TABLE = HugeType.VERTEX.string();

        public Vertex(String store) {
            super(joinTableName(store, TABLE));
        }

        @Override
        public void insert(Session session, BackendEntry entry) {
            long ttl = entry.ttl();
            if (ttl == 0L) {
                session.put(this.table(), CF, entry.id().asBytes(),
                            entry.columns());
            } else {
                session.put(this.table(), CF, entry.id().asBytes(),
                            entry.columns(), ttl);
            }
        }
    }

    public static class Edge extends HbaseTable {

        public static final String TABLE_SUFFIX = HugeType.EDGE.string();

        public Edge(String store, boolean out) {
            super(joinTableName(store, table(out)));
        }

        private static String table(boolean out) {
            // Edge out/in table
            return (out ? 'o' : 'i') + TABLE_SUFFIX;
        }

        public static Edge out(String store) {
            return new Edge(store, true);
        }

        public static Edge in(String store) {
            return new Edge(store, false);
        }

        @Override
        public void insert(Session session, BackendEntry entry) {
            long ttl = entry.ttl();
            if (ttl == 0L) {
                session.put(this.table(), CF, entry.id().asBytes(),
                            entry.columns());
            } else {
                session.put(this.table(), CF, entry.id().asBytes(),
                            entry.columns(), ttl);
            }
        }

        @Override
        protected void parseRowColumns(Result row, BackendEntry entry,
                                       Query query) throws IOException {
            /*
             * Collapse owner-vertex id from edge id, NOTE: unneeded to
             * collapse if BinarySerializer.keyWithIdPrefix set to true
             */
            byte[] key = row.getRow();
            key = Arrays.copyOfRange(key, entry.id().length(), key.length);

            long total = query.total();
            CellScanner cellScanner = row.cellScanner();
            while (cellScanner.advance() && total-- > 0) {
                Cell cell = cellScanner.current();
                assert CellUtil.cloneQualifier(cell).length == 0;
                entry.columns(BackendColumn.of(key, CellUtil.cloneValue(cell)));
            }
        }
    }

    public static class IndexTable extends HbaseTable {

        private static final long INDEX_DELETE_BATCH = Query.COMMIT_BATCH;
        protected final HugeType type;

        public IndexTable(String table, HugeType type) {
            super(table);
            this.type = type;
        }

        public HugeType type() {
            return this.type;
        }

        @Override
        public void insert(Session session, BackendEntry entry) {
            assert entry.columns().size() == 1;
            BackendColumn col = entry.columns().iterator().next();
            long ttl = entry.ttl();
            if (ttl == 0L) {
                session.put(this.table(), CF, col.name,
                            BinarySerializer.EMPTY_BYTES, col.value);
            } else {
                session.put(this.table(), CF, col.name,
                            BinarySerializer.EMPTY_BYTES, col.value, ttl);
            }
        }

        @Override
        public void eliminate(Session session, BackendEntry entry) {
            assert entry.columns().size() == 1;
            BackendColumn col = entry.columns().iterator().next();
            session.delete(this.table(), CF, col.name);
        }

        @Override
        public void delete(Session session, BackendEntry entry) {
            /*
             * Only delete index by label will come here
             * Regular index delete will call eliminate()
             */
            long count = 0L;
            for (BackendColumn column : entry.columns()) {
                session.commit();
                // Prefix query index label related indexes
                RowIterator iter = session.scan(this.table(), column.name);
                while (iter.hasNext()) {
                    session.delete(this.table(), CF, iter.next().getRow());
                    // Commit once reaching batch size
                    if (++count >= INDEX_DELETE_BATCH) {
                        session.commit();
                        count = 0L;
                    }
                }
            }
            if (count > 0L) {
                session.commit();
            }
        }

        @Override
        protected BackendEntryIterator newEntryIterator(Query query,
                                                        RowIterator rows) {
            return new BinaryEntryIterator<>(rows, query, (entry, row) -> {
                assert row.size() == 1;
                BackendColumn col = BackendColumn.of(row.getRow(), row.value());
                entry = new BinaryBackendEntry(query.resultType(), col.name);
                entry.columns(col);
                return entry;
            });
        }
    }

    public static class VertexLabelIndex extends IndexTable {

        public static final String TABLE = HugeType.VERTEX_LABEL_INDEX.string();

        public VertexLabelIndex(String store) {
            super(joinTableName(store, TABLE), HugeType.SECONDARY_INDEX);
        }
    }

    public static class EdgeLabelIndex extends IndexTable {

        public static final String TABLE = HugeType.EDGE_LABEL_INDEX.string();

        public EdgeLabelIndex(String store) {
            super(joinTableName(store, TABLE), HugeType.SECONDARY_INDEX);
        }
    }

    public static class SecondaryIndex extends IndexTable {

        public static final String TABLE = HugeType.SECONDARY_INDEX.string();

        public SecondaryIndex(String store) {
            super(joinTableName(store, TABLE), HugeType.SECONDARY_INDEX);
        }
    }

    public static class SearchIndex extends IndexTable {

        public static final String TABLE = HugeType.SEARCH_INDEX.string();

        public SearchIndex(String store) {
            super(joinTableName(store, TABLE), HugeType.SECONDARY_INDEX);
        }
    }

    public static class UniqueIndex extends IndexTable {

        public static final String TABLE = HugeType.UNIQUE_INDEX.string();

        public UniqueIndex(String store) {
            super(joinTableName(store, TABLE), HugeType.SECONDARY_INDEX);
        }
    }

    public static class RangeIndex extends IndexTable {

        public RangeIndex(String store, HugeType type) {
            super(joinTableName(store, type.string()), type);
        }

        public static RangeIndex rangeInt(String store) {
            return new RangeIndex(store, HugeType.RANGE_INT_INDEX);
        }

        public static RangeIndex rangeFloat(String store) {
            return new RangeIndex(store, HugeType.RANGE_FLOAT_INDEX);
        }

        public static RangeIndex rangeLong(String store) {
            return new RangeIndex(store, HugeType.RANGE_LONG_INDEX);
        }

        public static RangeIndex rangeDouble(String store) {
            return new RangeIndex(store, HugeType.RANGE_DOUBLE_INDEX);
        }
    }

    public static class ShardIndex extends IndexTable {

        public static final String TABLE = HugeType.SHARD_INDEX.string();

        public ShardIndex(String store) {
            super(joinTableName(store, TABLE), HugeType.SECONDARY_INDEX);
        }
    }
}