/*
 * 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.phoenix.coprocessor;

import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.CoprocessorEnvironment;
import org.apache.hadoop.hbase.DoNotRetryIOException;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.coprocessor.BaseEnvironment;
import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment;
import org.apache.hadoop.hbase.regionserver.HRegion;
import org.apache.hadoop.hbase.regionserver.RegionCoprocessorHost;
import org.apache.hadoop.hbase.security.User;
import org.apache.phoenix.query.QueryServices;
import org.apache.phoenix.query.QueryServicesOptions;
import org.apache.phoenix.schema.PIndexState;
import org.apache.phoenix.schema.PTable;
import org.apache.phoenix.schema.PTableType;


public class PhoenixMetaDataCoprocessorHost
        extends CoprocessorHost<PhoenixCoprocessor,PhoenixMetaDataCoprocessorHost.PhoenixMetaDataControllerEnvironment> {
    private RegionCoprocessorEnvironment env;
    public static final String PHOENIX_META_DATA_COPROCESSOR_CONF_KEY =
            "hbase.coprocessor.phoenix.classes";
    private static final String DEFAULT_PHOENIX_META_DATA_COPROCESSOR_CONF_KEY =
            "org.apache.phoenix.coprocessor.PhoenixAccessController";

    PhoenixMetaDataCoprocessorHost(RegionCoprocessorEnvironment env) throws IOException {
        super(null);
        this.env = env;
        this.conf = new Configuration();
        for (Entry<String, String> entry : env.getConfiguration()) {
            conf.set(entry.getKey(), entry.getValue());
        }
        boolean accessCheckEnabled = this.conf.getBoolean(QueryServices.PHOENIX_ACLS_ENABLED,
                QueryServicesOptions.DEFAULT_PHOENIX_ACLS_ENABLED);
        if (this.conf.get(PHOENIX_META_DATA_COPROCESSOR_CONF_KEY) == null && accessCheckEnabled) {
            this.conf.set(PHOENIX_META_DATA_COPROCESSOR_CONF_KEY, DEFAULT_PHOENIX_META_DATA_COPROCESSOR_CONF_KEY);
        }
       loadSystemCoprocessors(conf, PHOENIX_META_DATA_COPROCESSOR_CONF_KEY);
    }

    private ObserverGetter<PhoenixCoprocessor, MetaDataEndpointObserver> phoenixObserverGetter =
            PhoenixCoprocessor::getPhoenixObserver;
    
    private abstract class PhoenixObserverOperation extends ObserverOperationWithoutResult<MetaDataEndpointObserver> {
        PhoenixObserverOperation() {
            super(phoenixObserverGetter);
        }

        public PhoenixObserverOperation(User user) {
            super(phoenixObserverGetter, user);
        }

        public PhoenixObserverOperation(User user, boolean bypassable) {
            super(phoenixObserverGetter, user, bypassable);
        }
        
        void callObserver() throws IOException {
            Optional<MetaDataEndpointObserver> observer = phoenixObserverGetter.apply(getEnvironment().getInstance());
            if (observer.isPresent()) {
                call(observer.get());
            }
        }

        @Override
        protected void postEnvCall() {}
    }

    private boolean execOperation(
            final PhoenixObserverOperation ctx)
            throws IOException {
        if (ctx == null) return false;
        boolean bypass = false;
        for (PhoenixMetaDataControllerEnvironment env : coprocEnvironments) {
            if (env.getInstance() instanceof MetaDataEndpointObserver) {
                ctx.prepare(env);
                Thread currentThread = Thread.currentThread();
                ClassLoader cl = currentThread.getContextClassLoader();
                try {
                    currentThread.setContextClassLoader(env.getClassLoader());
                    ctx.callObserver();
                } catch (Throwable e) {
                    handleCoprocessorThrowable(env, e);
                } finally {
                    currentThread.setContextClassLoader(cl);
                }
                bypass |= ctx.shouldBypass();
                if (bypass) {
                    break;
                }
            }
            ctx.postEnvCall();
        }
        return bypass;
    }
    
    @Override
    protected void handleCoprocessorThrowable(final PhoenixMetaDataControllerEnvironment env, final Throwable e)
            throws IOException {
        if (e instanceof IOException) {
            if (e.getCause() instanceof DoNotRetryIOException) { throw (IOException)e.getCause(); }
        }
        super.handleCoprocessorThrowable(env, e);
    }

    /**
     * Encapsulation of the environment of each coprocessor
     */
    public static class PhoenixMetaDataControllerEnvironment extends BaseEnvironment<PhoenixCoprocessor>
            implements CoprocessorEnvironment<PhoenixCoprocessor> {

        private RegionCoprocessorEnvironment env;

        PhoenixMetaDataControllerEnvironment(RegionCoprocessorEnvironment env, PhoenixCoprocessor instance,
                int priority, int sequence, Configuration conf) {
            super(instance, priority, sequence, conf);
            this.env = env;
        }
        
        public RegionCoprocessorHost getCoprocessorHost() {
            return ((HRegion)env.getRegion()).getCoprocessorHost();
        }

        RegionCoprocessorEnvironment getRegionCoprocessorEnvironment() {
            return env;
        }
    }
    

    void preGetTable(final String tenantId, final String tableName, final TableName physicalTableName)
            throws IOException {
        execOperation(new PhoenixObserverOperation() {
            @Override
            public void call(MetaDataEndpointObserver observer) throws IOException {
                observer.preGetTable(this, tenantId, tableName, physicalTableName);
            }
        });
    }

    void preCreateTable(final String tenantId, final String tableName, final TableName physicalTableName,
            final TableName parentPhysicalTableName, final PTableType tableType, final Set<byte[]> familySet, final Set<TableName> indexes)
            throws IOException {
        execOperation(new PhoenixObserverOperation() {
            @Override
            public void call(MetaDataEndpointObserver observer) throws IOException {
                observer.preCreateTable(this, tenantId, tableName, physicalTableName, parentPhysicalTableName, tableType,
                        familySet, indexes);
            }
        });
    }

    void preCreateViewAddChildLink(final String tableName) throws IOException {
        execOperation(new PhoenixObserverOperation() {
            @Override
            public void call(MetaDataEndpointObserver observer) throws IOException {
                observer.preCreateViewAddChildLink(this, tableName);
            }
        });
    }

    void preDropTable(final String tenantId, final String tableName, final TableName physicalTableName,
            final TableName parentPhysicalTableName, final PTableType tableType, final List<PTable> indexes) throws IOException {
        execOperation(new PhoenixObserverOperation() {
            @Override
            public void call(MetaDataEndpointObserver observer) throws IOException {
                observer.preDropTable(this, tenantId, tableName, physicalTableName, parentPhysicalTableName, tableType, indexes);
            }
        });
    }

    void preAlterTable(final String tenantId, final String tableName, final TableName physicalTableName,
            final TableName parentPhysicalTableName, final PTableType type) throws IOException {
        execOperation(new PhoenixObserverOperation() {
            @Override
            public void call(MetaDataEndpointObserver observer) throws IOException {
                observer.preAlterTable(this, tenantId, tableName, physicalTableName, parentPhysicalTableName, type);
            }
        });
    }

    void preGetSchema(final String schemaName) throws IOException {
        execOperation(new PhoenixObserverOperation() {
            @Override
            public void call(MetaDataEndpointObserver observer) throws IOException {
                observer.preGetSchema(this, schemaName);
            }
        });
    }

    public void preCreateSchema(final String schemaName) throws IOException {

        execOperation(new PhoenixObserverOperation() {
            @Override
            public void call(MetaDataEndpointObserver observer) throws IOException {
                observer.preCreateSchema(this, schemaName);
            }
        });
    }

    void preDropSchema(final String schemaName) throws IOException {
        execOperation(new PhoenixObserverOperation() {
            @Override
            public void call(MetaDataEndpointObserver observer) throws IOException {
                observer.preDropSchema(this, schemaName);
            }
        });
    }

    void preIndexUpdate(final String tenantId, final String indexName, final TableName physicalTableName,
            final TableName parentPhysicalTableName, final PIndexState newState) throws IOException {
        execOperation(new PhoenixObserverOperation() {
            @Override
            public void call(MetaDataEndpointObserver observer) throws IOException {
                observer.preIndexUpdate(this, tenantId, indexName, physicalTableName, parentPhysicalTableName, newState);
            }
        });
    }

    @Override
    public PhoenixCoprocessor checkAndGetInstance(Class<?> implClass)
            throws InstantiationException, IllegalAccessException {
        if (PhoenixCoprocessor.class
                .isAssignableFrom(implClass)) { return (PhoenixCoprocessor)implClass.newInstance(); }
        return null;
    }

    @Override
    public PhoenixMetaDataControllerEnvironment createEnvironment(PhoenixCoprocessor instance, int priority,
            int sequence, Configuration conf) {
        return new PhoenixMetaDataControllerEnvironment(env, instance, priority, sequence, conf);
    }
}