/*
 * Copyright 2019 Arcus Project
 *
 * 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.iris.agent.zwave.db;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

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

import com.almworks.sqlite4java.SQLiteConnection;
import com.almworks.sqlite4java.SQLiteStatement;
import com.iris.agent.config.ConversionService;
import com.iris.agent.db.Db;
import com.iris.agent.db.DbExtractor;
import com.iris.agent.db.DbService;
import com.iris.agent.zwave.ZWException;
import com.iris.agent.zwave.ZWNetwork;
import com.iris.agent.zwave.node.ZWNode;

public class ZWDao {
   private static final Logger logger = LoggerFactory.getLogger(ZWDao.class);
   
   private static final Object LOCK = new Object();
   private static final Map<String, String> config = Collections.synchronizedMap(new HashMap<>());
   private static final Set<Integer> knownNodes = new HashSet<>();
   
   private static final String NODES_QUERY = "SELECT node_id, basic_type, generic_type, specific_type, man_id, type_id, product_id, cmdclasses, online, offline_timeout FROM zip_node";
   
   private static final String READ_NODE = "SELECT node_id, basic_type, generic_type, specific_type, man_id, type_id, product_id, cmdclasses, online, offline_timeout FROM zip_node WHERE node_id=?";
   private static final String CREATE_NODE = "INSERT INTO zip_node(node_id, basic_type ,generic_type ,specific_type, man_id, type_id ,product_id, cmdclasses, online, offline_timeout) VALUES (?,?,?,?,?,?,?,?,?,?)";
   private static final String UPDATE_NODE = "UPDATE zip_node SET node_id=?, basic_type=?, generic_type=?, specific_type=?, man_id=?, type_id=?, product_id=?, cmdclasses=?, online=?, offline_timeout=? WHERE node_id=?";
   private static final String DELETE_NODE = "DELETE FROM zip_node WHERE node_id=?";
   private static final String DELETE_ALL = "DELETE FROM zip_node";
   
   private static final String CHECK_CONFIG_TABLE = "SELECT name FROM sqlite_master WHERE type='table' and name='zip_config'";
   private static final String CONFIG_QUERY = "SELECT key, value FROM zip_config";
   
   private static final String[] SCHEMA_SCRIPTS = {
         "/sql/zip.sql"
   };
        
   private static Db db;
   
   private static NetworkRecord network;
   
   private ZWDao() {      
   }
   
   //////////////////////////////////////////////////////////////////////////////
   // Startup and shutdown
   /////////////////////////////////////////////////////////////////////////////
   
   static void setupSchema(Db db) {
      int curVersion = 0;

      if(configTableExists(db)) {
         String schema = db.query("SELECT value FROM zip_config WHERE key=?", Binders.ConfigBinder.INSTANCE, "schema", Extractors.ConfigExtractor.INSTANCE);
         Integer schemaver = ConversionService.to(Integer.class, schema);
         if (schemaver != null) {
            curVersion = schemaver;
         }
      }

      for(int i = curVersion; i < SCHEMA_SCRIPTS.length; i++) {
         logger.debug("executing sql script {}", SCHEMA_SCRIPTS[i]);
         if (db != null) {
            db.execute(ZWDao.class.getResource(SCHEMA_SCRIPTS[i]));
         } else {
            throw new RuntimeException("could not start zip dao: null db");
         }
      }
   }

   public static void start() {
      synchronized (LOCK) {
         if (db != null) {
            throw new ZWException("zip dao already started");
         }

         db = DbService.get();
         setupSchema(db);

         try {
            long start = System.nanoTime();
            List<KeyValuePair> all = getConfig();
            for (KeyValuePair kv : all) {
               config.put(kv.getKey(), kv.getValue());
            }
            double elapsed = (System.nanoTime() - start) / 1000000000.0;
            logger.info("loaded {} zwave configuration records in {}s", all.size(), String.format("%.3f", elapsed));
         } catch (Exception ex) {
            logger.warn("failed to preload zwave configuration:", ex);
         }
         
         long homeId =        get(NetworkRecord.CONFIG_HOMEID, Long.class, ZWNetwork.NO_HOME_ID).longValue();
         long networkKeyMsb = get(NetworkRecord.CONFIG_NWKKEYMSB, Long.class, ZWNetwork.NO_NETWORK_KEY_MSB).longValue();
         long networkKeyLsb = get(NetworkRecord.CONFIG_NWKKEYLSB, Long.class, ZWNetwork.NO_NETWORK_KEY_LSB).longValue();

         network = new NetworkRecord(homeId, networkKeyMsb, networkKeyLsb);
      }
   }

   public static void shutdown() {
      synchronized (LOCK) {
         db = null;
         config.clear();
      }
   }

   static void drop() {
      get().execute("DROP TABLE IF EXISTS zip_config");
      get().execute("DROP TABLE IF EXISTS zip_node");
   }
   
   
   /////////////////////////////////////////////////////////////////////////////
   // Configuration support
   /////////////////////////////////////////////////////////////////////////////
   
   static boolean configTableExists(Db db) {
      String name = db.query(CHECK_CONFIG_TABLE, new DbExtractor<String>() {
         
         @Override
         public String extract(SQLiteConnection conn, SQLiteStatement stmt) throws Exception {
            return stmt.columnString(0);
         }
      });

      return name != null;
   }
   
   public static List<KeyValuePair> getConfig() {
      return DbService.get().queryAll(CONFIG_QUERY, Extractors.ConfigAllExtractor.INSTANCE);
   }
   
   static <T> void put(String key, T value) {
      String svalue = ConversionService.from(value);

      // write through cache
      config.put(key,svalue);

      KeyValuePair pair = new KeyValuePair(key,svalue);
      DbService.get().execute("INSERT OR REPLACE INTO zip_config (key,value) VALUES (?,?)", Binders.ConfigInsertBinder.INSTANCE, pair);
   }
   
   static <T> void put(Db db, String key, T value) {
      String svalue = ConversionService.from(value);
      KeyValuePair pair = new KeyValuePair(key,svalue);
      db.execute("INSERT OR REPLACE INTO zip_config (key,value) VALUES (?,?)", Binders.ConfigInsertBinder.INSTANCE, pair);
   }
   
   static Db get() {
      if (db == null) {
         throw new ZWException("zip dao not started");
      }
      return db;
   }
   
   static <T> T get(String key, Class<T> type) {
      String result = config.get(key);
      return ConversionService.to(type, result);
   }
   
   static <T> T get(String key, Class<T> type, T def) {
      T result = get(key, type);
      return (result == null) ? def : result;
   }
   
   /////////////////////////////////////////////////////////////////////////////
   // Network persistence support
   /////////////////////////////////////////////////////////////////////////////
   
   public static NetworkRecord getNetwork() {
      if (network == null) {
         throw new ZWException("zip dao not started");
      }
      return network;
   }
   
   public static void updateHomeId(long homeId) {
      network.setHomeId(homeId);
      put(db, NetworkRecord.CONFIG_HOMEID, network.getHomeId());
   }
   
   /*
   public static void updateNetwork() {
      synchronized (LOCK) {
         if (network.needsUpdate()) {
            Db db = get();
            
            put(db, NetworkRecord.CONFIG_NWKKEYLSB, network.getNetworkKeyLsb());
            put(db, NetworkRecord.CONFIG_NWKKEYMSB, network.getNetworkKeyMsb());
            network.updated();
         }import com.iris.bootstrap.guice.Binders;

      }
   }
   */
   
   public static void deleteNetwork() {
      put(NetworkRecord.CONFIG_HOMEID, null);
      network.invalidate();
      knownNodes.clear();
      get().execute(DELETE_ALL);
   }
   
   /////////////////////////////////////////////////////////////////////////////
   // Node persistence support
   /////////////////////////////////////////////////////////////////////////////
   
   public static List<ZWNode> getAllNodes() {
      List<ZWNode> nodes =  DbService.get().queryAll(NODES_QUERY, Extractors.NodeExtractor.INSTANCE);
      if (nodes != null) {
         knownNodes.clear();
         nodes.forEach(n -> knownNodes.add(n.getNodeId()));
      }
      return nodes;
   }
   
   public static ZWNode getNode() {
      return DbService.get().query(READ_NODE, Extractors.NodeExtractor.INSTANCE);
   }
   
   public static void saveNode(ZWNode node) {
      if (knownNodes.contains(node.getNodeId())) {
         updateNode(node);
      }
      else {
         createNode(node);
      }
   }
   
   public static void createNode(ZWNode node) {
      Db db = get();
      db.execute(CREATE_NODE, Binders.CreateNodeBinder.INSTANCE, node);
      knownNodes.add(node.getNodeId());
   }
   
   public static void updateNode(ZWNode node) {
      Db db = get();
      db.execute(UPDATE_NODE, Binders.UpdateNodeBinder.INSTANCE, node);
   }
   
   public static void deleteNode(ZWNode node) {
      knownNodes.remove(node.getNodeId());
      Db db = get();
      db.execute(DELETE_NODE, Binders.DeleteNodeBinder.INSTANCE, node);
   }
}