/*
 * 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.empire.samples.db.advanced;

import java.io.Serializable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Connection;
import java.sql.DriverManager;
import java.time.LocalDate;
import java.util.List;
import java.util.Locale;

import org.apache.empire.commons.DateUtils;
import org.apache.empire.commons.ObjectUtils;
import org.apache.empire.commons.StringUtils;
import org.apache.empire.data.DataType;
import org.apache.empire.data.list.DataListEntry;
import org.apache.empire.db.DBCmdParam;
import org.apache.empire.db.DBCommand;
import org.apache.empire.db.DBDDLGenerator.DDLActionType;
import org.apache.empire.db.DBQuery;
import org.apache.empire.db.DBReader;
import org.apache.empire.db.DBRecord;
import org.apache.empire.db.DBSQLScript;
import org.apache.empire.db.DBTableColumn;
import org.apache.empire.db.exceptions.ConstraintViolationException;
import org.apache.empire.db.exceptions.RecordUpdateFailedException;
import org.apache.empire.db.exceptions.StatementFailedException;
import org.apache.empire.db.expr.compare.DBCompareExpr;
import org.apache.empire.dbms.DBMSHandler;
import org.apache.empire.samples.db.advanced.db.CarSalesDB;
import org.apache.empire.samples.db.advanced.db.CarSalesDB.DealerRating;
import org.apache.empire.samples.db.advanced.db.CarSalesDB.DealershipType;
import org.apache.empire.samples.db.advanced.db.CarSalesDB.EngineType;
import org.apache.empire.samples.db.advanced.db.DealerSalesView;
import org.apache.empire.samples.db.advanced.records.BrandRecord;
import org.apache.empire.samples.db.advanced.records.DealerRecord;
import org.apache.empire.samples.db.advanced.records.ModelRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


public class SampleAdvApp 
{
    // Logger
    private static final Logger log = LoggerFactory.getLogger(SampleAdvApp.class);
    
    private static SampleAdvConfig config = new SampleAdvConfig();


    private final CarSalesDB carSales = new CarSalesDB();
    
    private SampleContext context;

    /**
     * <PRE>
     * This is the entry point of the Empire-DB Sample Application
     * Please check the config.xml configuration file for Database and Connection settings.
     * </PRE>
     * @param args arguments
     */
    public static void main(String[] args)
    {
        // Init Configuration
        config.init((args.length > 0 ? args[0] : "config.xml" ));
        // create the app
        SampleAdvApp app = new SampleAdvApp();
        try
        {   // Run
            log.info("Running DB Sample...");
            app.run();
            // Done
            log.info("DB Sample finished successfully.");
        } catch (Exception e) {
            // Error
            log.error("Running SampleApp failed with Exception" + e.toString(), e);
            if (app.context!=null)
                app.context.rollback();
        } finally {
            if (app.context!=null)
                app.context.discard();
        }
    }

    /**
     * This method runs all the example code
     */
    public void run()
    {
        // STEP 1: Get a JDBC Connection
        System.out.println("*** Step 1: getJDBCConnection() ***");
        Connection conn = getJDBCConnection();

        // STEP 2: Choose a dbms
        System.out.println("*** Step 2: getDatabaseProvider() ***");
        DBMSHandler dbms = getDBMSHandler(config.getDatabaseProvider());
        
        // STEP 2.2: Create a Context
        context = new SampleContext(carSales, dbms, conn);
        // set optional context features
        context.setPreparedStatementsEnabled(true);
        context.setRollbackHandlingEnabled(false);

        /*
        // optional: Disable PreparedStatements for DBRecord operations (not recommended)
        if (!context.isPreparedStatementsEnabled())
            carSales.setPreparedStatementsEnabled(false);
        */    
        
        // STEP 3: Open Database (and create if not existing)
        System.out.println("*** Step 3: openDatabase() ***");
        // db.open(context);
        carSales.open(context);
        if (carSales.wasCreated())
        {   // newly created
            populateDatabase();            
            insertSalesUsingBatch();
            context.commit();
        }
        
        // DQL-Demos
        simpleQueryDemo();
        queryViewDemo();
        subqueryQueryDemo();
        paramQueryDemo();

        // DML-Demos
        modelPriceIncrease();
        modelPriceDecrease();
        dealerRatingUpdate();
        
        // Remember RollbackHandling
        boolean prevRBHandling = context.isRollbackHandlingEnabled(); 
        try {
            // commit pending operations (just to be sure)
            context.commit();
            // run transaction demo
            transactionDemo();
        } finally {
            context.setRollbackHandlingEnabled(prevRBHandling);
        }
        
        cascadeDeleteDemo();
        
        ddlDemo("Beadles Volkswagen", "www.group1auto.co.uk", "https://www.group1auto.co.uk/volkswagen/locations/beadles-volkswagen-dartford");
        
        // Done
        log.info("DB Sample Advanced finished successfully.");
        context.commit();
    }

    public void populateDatabase()
    {
        // Add some brands
        BrandRecord brandVW    = (new BrandRecord(context)).insert("WVW", "Volkswagen", "Germany");
        BrandRecord brandFord  = (new BrandRecord(context)).insert("1F",  "Ford",       "USA");
        BrandRecord brandTesla = (new BrandRecord(context)).insert("5YJ", "Tesla",      "USA");
        BrandRecord brandToy   = (new BrandRecord(context)).insert("JT",  "Toyota",     "Japan");
        
        // Add some models
        ModelRecord model = new ModelRecord(context);
        model.insert(brandVW,   "Golf",     "Golf Style 1,5 l TSI",         "Style",         EngineType.P, 130, 30970d, LocalDate.of(2019, 10, 24));
        model.insert(brandVW,   "Golf",     "Golf R-Line 2,0 l TSI 4MOTION","R-Line",        EngineType.P, 190, 38650d, LocalDate.of(2019, 10, 24));
        model.insert(brandVW,   "Tiguan",   "Tiguan Life 1,5 l TSI",        "Life",          EngineType.P, 150, 32545d, LocalDate.of(2016, 01, 01));
        model.insert(brandVW,   "Tiguan",   "Tiguan Elegance 2,0 l TDI SCR","Elegance",      EngineType.D, 150, 40845d, LocalDate.of(2016, 01, 01));
        model.insert(brandVW,   "Tiguan",   "Tiguan R-Line 1,4 l eHybrid",  "R-Line",        EngineType.H, 150, 48090d, LocalDate.of(2016, 01, 01));
        model.insert(brandVW,   "Mulitvan", "Multivan 6.1 Highline 2.0TDI", "Highline",      EngineType.D, 204, 84269d, LocalDate.of(2019, 10, 21));
        // Tesla
        model.insert(brandTesla,"Model 3",  "Model 3 LR",                   "Long Range",    EngineType.E, 261, 45940d, LocalDate.of(2017, 07, 01));
        model.insert(brandTesla,"Model 3",  "Model 3 Performance",          "Performance",   EngineType.E, 487, 53940d, LocalDate.of(2017, 07, 01));
        model.insert(brandTesla,"Model Y",  "Model Y LR",                   "Long Range",    EngineType.E, 345, 53940d, LocalDate.of(2020, 03, 13));
        model.insert(brandTesla,"Model Y",  "Model Y Performance",          "Performance",   EngineType.E, 450, 58940d, LocalDate.of(2020, 03, 13));
        model.insert(brandTesla,"Model S",  "Model S Plaid",                "Plaid",         EngineType.E, 1020,    0d, LocalDate.of(2021, 12, 01));
        // Ford
        model.insert(brandFord, "Mustang",  "Mustang GT 5,0 l Ti-VCT V8",           "GT",    EngineType.P, 449, 54300d, LocalDate.of(2017, 01, 01));
        model.insert(brandFord, "Mustang",  "Mustang Mach1 5,0 l Ti-VCT V8",        "Mach1", EngineType.P, 460, 62800d, LocalDate.of(2020, 10, 16));
        // Toyota
        model.insert(brandToy,  "Prius",    "Prius Hybrid 1,8-l-VVT-i",             "Basis", EngineType.H, 122, 38000d, LocalDate.of(2017, 01, 01));    
        model.insert(brandToy,  "Supra",    "GR Supra Pure 2,0 l Twin-Scroll Turbo","Pure",  EngineType.P, 258, 49290d, LocalDate.of(2020, 03, 31));
        
        // Add some dealers
        DealerRecord dealerDE = (new DealerRecord(context)).insert("Autozentrum Schmitz",      "Munich",       "Germany");
        DealerRecord dealerUK = (new DealerRecord(context)).insert("Beadles Volkswagen",       "Dartford",     "United Kingdom");
        DealerRecord dealerUS = (new DealerRecord(context)).insert("Auto Nation",              "Los Angeles",  "USA");
        DealerRecord dealerFR = (new DealerRecord(context)).insert("Vauban Motors Argenteuil", "Paris",        "France");
        DealerRecord dealerIT = (new DealerRecord(context)).insert("Autorigoldi S.p.A.",       "Milan",        "Italy");
        
        // Add brands to dealers
        CarSalesDB.DealerBrands DB = carSales.DEALER_BRANDS;
        DBRecord rec = new DBRecord(context, DB);
        rec.create(DBRecord.key(dealerDE, brandTesla)).set(DB.DEALERSHIP_TYPE, DealershipType.B).update();
        rec.create(DBRecord.key(dealerDE, brandVW))   .set(DB.DEALERSHIP_TYPE, DealershipType.U).update();
        rec.create(DBRecord.key(dealerUK, brandVW))   .set(DB.DEALERSHIP_TYPE, DealershipType.B).update();
        rec.create(DBRecord.key(dealerUK, brandFord)) .set(DB.DEALERSHIP_TYPE, DealershipType.I).update();
        rec.create(DBRecord.key(dealerUS, brandFord)) .set(DB.DEALERSHIP_TYPE, DealershipType.B).update();
        rec.create(DBRecord.key(dealerUS, brandTesla)).set(DB.DEALERSHIP_TYPE, DealershipType.I).update();
        rec.create(DBRecord.key(dealerFR, brandToy))  .set(DB.DEALERSHIP_TYPE, DealershipType.B).update();
        rec.create(DBRecord.key(dealerIT, brandToy))  .set(DB.DEALERSHIP_TYPE, DealershipType.G).update();
        rec.create(DBRecord.key(dealerIT, brandVW))   .set(DB.DEALERSHIP_TYPE, DealershipType.B).update();
        
    }
    
    private void insertSalesUsingBatch()
    {
        CarSalesDB db = this.carSales;
        DBSQLScript batch = new DBSQLScript(context);
        // get list of all dealers
        DBCommand cmd = context.createCommand();
        cmd.select(db.DEALER.ID);
        List<Long> dealerIdList = context.getUtils().querySimpleList(Long.class, cmd);
        // get all models
        cmd.clear();
        cmd.select(db.MODEL.ID, db.MODEL.SPECIFICATION, db.MODEL.BASE_PRICE);  // select the ones we need (optional)
        // no constraints on model
        List<DataListEntry> modelList = context.getUtils().queryDataList(cmd);
        for (DataListEntry model : modelList)
        {
            int count = generateRandomSales(batch, model, dealerIdList);
            log.info("{} Sales added for Model {}", count, model.getString(db.MODEL.SPECIFICATION));
        }
        // execute the batch
        int count = batch.executeBatch();
        log.info("{} Sales added with batch", count);
    }
    
    private int generateRandomSales(DBSQLScript batch, DataListEntry model, List<Long> dealerIdList)
    {
        CarSalesDB db = this.carSales;
        BigDecimal price = model.getDecimal(db.MODEL.BASE_PRICE);
        if (ObjectUtils.isEmpty(price))
            return 0;
        int count = 0;
        int baseYear = LocalDate.now().getYear()-3;
        DBCommand cmd = context.createCommand();
        for (int i = (int)(Math.random()*99)+25; i>0; i--)
        {
            int dealIdx = (int)(Math.random()*dealerIdList.size());
            int year  = (int)(Math.random()*3)+baseYear;
            int month = (int)(Math.random()*12)+1;
            BigDecimal variation = new BigDecimal((Math.random()*200) - 100.0);
            variation = variation.setScale(2, RoundingMode.HALF_UP);
            cmd.set(db.SALES.MODEL_ID.to(model.getRecordId(db.MODEL)))
               .set(db.SALES.DEALER_ID.to(dealerIdList.get(dealIdx)))
               .set(db.SALES.YEAR.to(year))
               .set(db.SALES.MONTH.to(month))
               .set(db.SALES.PRICE.to(price.add(variation)));
            // add to batch
            batch.addInsert(cmd);
            count++;
        }
        return count;
    }
    
    /**
     * The result Bean for the query Example
     * @author rainer
     */
    public static class QueryResult implements Serializable
    {
        private static final long serialVersionUID = 1L;
        
        private String brand;
        private String model;
        private BigDecimal basePrice;
        private int salesCount;
        private BigDecimal avgSalesPrice;
        private BigDecimal priceDifference;

        /**
         * Fields constructor for QueryResult
         * Must match the SELECT phrase and will be used by queryBeanList()
         * @param brand
         * @param model
         * @param basePrice
         * @param salesCount
         * @param avgSalesPrice
         * @param priceDifference
         */
        public QueryResult(String brand, String model, BigDecimal basePrice
                         , int salesCount, BigDecimal avgSalesPrice, BigDecimal priceDifference)
        {
            this.brand = brand;
            this.model = model;
            this.basePrice = basePrice;
            this.salesCount = salesCount;
            this.avgSalesPrice = avgSalesPrice;
            this.priceDifference = priceDifference;
        }

        public String getBrand()
        {
            return brand;
        }

        public String getModel()
        {
            return model;
        }

        public BigDecimal getBasePrice()
        {
            return basePrice;
        }

        public int getSalesCount()
        {
            return salesCount;
        }

        public BigDecimal getAvgSalesPrice()
        {
            return avgSalesPrice;
        }

        public BigDecimal getPriceDifference()
        {
            return priceDifference;
        }
    }

    public void subqueryQueryDemo()
    {
        // shortcuts (for convenience)
        CarSalesDB.Model MODEL = carSales.MODEL;
        CarSalesDB.Sales SALES = carSales.SALES;

        // create command
        // DBColumnExpr SALES_COUNT = SALES.count();
        DBCommand sub = context.createCommand()
           .selectQualified(SALES.MODEL_ID, SALES.count())
           .groupBy(SALES.MODEL_ID);
        DBQuery q = new DBQuery(sub);
        
        // create command
        DBCommand cmd = context.createCommand()
           .select(MODEL.SPECIFICATION, q.column(SALES.count()))
           .join(MODEL.ID, q.column(SALES.MODEL_ID));
        
        List<DataListEntry> list = context.getUtils().queryDataList(cmd);
        for (DataListEntry dle : list)
        {
            System.out.println(dle.toString());
        }
        
    }
    
    public void simpleQueryDemo()
    {
        // shortcuts (for convenience)
        CarSalesDB.Brand BRAND = carSales.BRAND;
        CarSalesDB.Model MODEL = carSales.MODEL;
        CarSalesDB.Sales SALES = carSales.SALES;

        // .selectQualified(BRAND.NAME, MODEL.CONFIG_NAME) 
        
        // create a command
        DBCommand cmd = context.createCommand()
           .select  (BRAND.NAME, MODEL.SPECIFICATION, MODEL.BASE_PRICE)
           .select  (SALES.MODEL_ID.count(), SALES.PRICE.avg())
           .select  (SALES.PRICE.avg().minus(MODEL.BASE_PRICE.avg()).round(2).as("DIFFERENCE"))
           .join    (MODEL.WMI, BRAND.WMI)
           .joinLeft(MODEL.ID.on(SALES.MODEL_ID).and(SALES.YEAR.is(2021)))  // only year 2021
           .where   (MODEL.ENGINE_TYPE.in(EngineType.P, EngineType.H, EngineType.E)) // Petrol, Hybrid, Electric
           .where   (MODEL.BASE_PRICE.isGreaterThan(30000))
           .groupBy (BRAND.NAME, MODEL.SPECIFICATION, MODEL.BASE_PRICE)
           .having  (SALES.MODEL_ID.count().isGreaterThan(5))   // more than 5 sales
           .orderBy (BRAND.NAME.desc(), MODEL.SPECIFICATION.asc());
             
        // Return a list of Java beans (needs matching fields constructor or setter methods)           
        // This is just one of several options to obtain an process query results          
        List<QueryResult> list = context.getUtils().queryBeanList(cmd, QueryResult.class, null);
        log.info("queryBeanList returnes {} items", list.size());
        
        /*
        List<DataListEntry> list = context.getUtils().queryDataList(cmd);
        for (DataListEntry dle : list)
        {
            System.out.println(dle.toString());
        }
        */
        /*
        DataListEntry entry = context.getUtils().queryDataEntry(cmd);
        for (int i=0; i<entry.getFieldCount(); i++)
            log.info("col {} -> {}", entry.getColumn(i).getName(), entry.getColumn(i).getBeanPropertyName());
        */    
    }

    public void paramQueryDemo()
    {
        // shortcuts (for convenience)
        CarSalesDB.Brand BRAND = carSales.BRAND;
        CarSalesDB.Model MODEL = carSales.MODEL;
        
        // create command
        DBCommand cmd = context.createCommand();
        // create params
        DBCmdParam brandParam = cmd.addParam();
        DBCmdParam engineTypeParam = cmd.addParam();
                
        // create the command
        cmd.select  (BRAND.NAME, MODEL.SPECIFICATION, MODEL.BASE_PRICE, MODEL.ENGINE_TYPE, MODEL.ENGINE_POWER)
           .join    (MODEL.WMI, BRAND.WMI)
           .where   (BRAND.NAME.is(brandParam))
           .where   (MODEL.ENGINE_TYPE.is(engineTypeParam))
           .orderBy (BRAND.NAME.desc(), MODEL.SPECIFICATION.asc());

        // set the params
        brandParam.setValue("Tesla");
        engineTypeParam.setValue(EngineType.E);
        // and run
        List<DataListEntry> list = context.getUtils().queryDataList(cmd);
        for (DataListEntry dle : list)
            System.out.println(dle.toString());
        
        // change params
        brandParam.setValue("Volkswagen");
        engineTypeParam.setValue(EngineType.P);
        // and run again
        list = context.getUtils().queryDataList(cmd);
        for (DataListEntry dle : list)
            System.out.println(dle.toString());
        
    }
    
    public void modelPriceIncrease()
    {
        // shortcuts (for convenience)
        CarSalesDB.Brand BRAND = carSales.BRAND;
        CarSalesDB.Model MODEL = carSales.MODEL;
        // create command

        // Sales-Info
        String salesInfo = "Price update "+DateUtils.formatDate(LocalDate.now(), Locale.US);
        // create command
        DBCommand cmd = context.createCommand()
            // increase model base prices by 5%
            .select(BRAND.NAME, MODEL.SPECIFICATION,  MODEL.BASE_PRICE, MODEL.BASE_PRICE.multiplyWith(105).divideBy(100).round(2).as("NEW_PRICE"))
            // set update fields
            .set   (MODEL.BASE_PRICE.to(MODEL.BASE_PRICE.multiplyWith(105).divideBy(100).round(2)))
            .set   (MODEL.SALES_INFO.to(salesInfo))
            // join with BRANDS
            .join  (MODEL.WMI, BRAND.WMI) // , BRAND.NAME.is("Tesla")
            // on all Volkswagen with Diesel engine
            .where(BRAND.NAME.upper().like("VOLKSWAGEN"))
            .where(MODEL.ENGINE_TYPE.is(EngineType.D)); // (MODEL.NAME.is("Tiguan").and(
        
        // Preview the change
        for (DataListEntry item : context.getUtils().queryDataList(cmd))
            System.out.println(item.toString());

        /*
        // cmd.removeWhereConstraint(MODEL.NAME.is("Tiguan").and(MODEL.ENGINE_TYPE.is(EngineType.D)));
        // cmd.removeWhereConstraintOn(BRAND.NAME);
        sql = cmd.getUpdate();
        params = cmd.getParamValues();
        System.out.println(sql);
        System.out.println(StringUtils.arrayToString(params, "|"));
        */
        
        // Execute the change
        int count = context.executeUpdate(cmd);
        log.info("{} models affected", count);
        context.commit();
    }
    
    public void modelPriceDecrease()
    {
        // shortcuts (for convenience)
        CarSalesDB.Brand BRAND = carSales.BRAND;
        CarSalesDB.Model MODEL = carSales.MODEL;
        // create command
        DBCommand cmd = context.createCommand()
            .select(MODEL.getColumns())
            .join (MODEL.WMI, BRAND.WMI)
            .where(BRAND.NAME.upper().like("VOLKSWAGEN"))
            .where(MODEL.ENGINE_TYPE.is(EngineType.D)); // (MODEL.NAME.is("Tiguan").and(

        // Use DBReader to process query result
        DBReader reader = new DBReader(context);
        try
        {   // Open Reader
            log.info("running modelPriceDecrease for cmd {}", cmd.getSelect());
            reader.open(cmd);
            // Print output
            ModelRecord record = new ModelRecord(context);
            // Disable rollback handling to improve performance
            record.setRollbackHandlingEnabled(false);
            BigDecimal factor = (new BigDecimal(1.05d)).setScale(2, RoundingMode.HALF_UP);
            while (reader.moveNext())
            {   // Init updateable record
                reader.initRecord(record);
                // Calculate Base-Price
                BigDecimal oldPrice = reader.getDecimal(MODEL.BASE_PRICE);
                BigDecimal newPrice = oldPrice.divide(factor, 2, RoundingMode.HALF_UP);
                record.set(MODEL.BASE_PRICE, newPrice);
                // update
                record.update();
            }
            // Done
            context.commit();
        } finally {
            // always close Reader
            reader.close();
        }
    }

    private void dealerRatingUpdate()
    {
        CarSalesDB.Dealer DEALER = carSales.DEALER;
        // Clear all dealer ratings
        DBCommand cmd = context.createCommand()
                .set(DEALER.RATING.to(DealerRating.X))
                .where(DEALER.RATING.isNot(null));
        context.executeUpdate(cmd);
        context.commit();
        
        // Subquery to find TOP 3 dealers
        CarSalesDB.Sales SALES = carSales.SALES;
        DBCommand qryCmd = context.createCommand()
            .select(SALES.DEALER_ID)
            .where(SALES.YEAR.is(LocalDate.now().getYear()-1))
            .groupBy(SALES.DEALER_ID)
            .orderBy(SALES.PRICE.sum().desc())
            .limitRows(3);
        DBQuery qryTop = new DBQuery(qryCmd, "qtop");
        
        // Dealer-Query
        cmd.clear();
        cmd.join(DEALER.ID, qryTop.column(SALES.DEALER_ID));
        
        int index = 0;
        List<DealerRecord> list = context.getUtils().queryRecordList(cmd, DEALER, DealerRecord.class);
        for (DealerRecord dealer : list)
        {
             DealerRating oldRating = dealer.getEnum(DEALER.RATING);
             DealerRating newRating = DealerRating.values()[index++];
             log.info("Dealer \"{}\" rating changed from {} to {}", dealer.getString(DEALER.COMPANY_NAME), oldRating, newRating);
             dealer.set(DEALER.RATING, newRating);
             dealer.update();
             // Show brands
             String brands = dealer.getBrands();
             log.info("Dealer \"{}\" distributes the brands {}", dealer.getString(DEALER.COMPANY_NAME), brands);
             // alternatively use  
             // List<BrandRecord> brandList = dealer.getBrandList();
        }
        context.commit();
    }
    
    private void queryViewDemo()
    {
        // query all
        /*
        DBCommand cmd = context.createCommand()
           .select  (DSV.getColumns())
           .orderBy (DSV.SALE_YEAR, DSV.DEALER_COUNTRY);
        List<DataListEntry> list = context.getUtils().queryDataList(cmd);
        for (DataListEntry dle : list)
        {
            System.out.println(dle.toString());
        }
        */
        
        // shortcuts (for convenience)
        DealerSalesView DSV = carSales.DEALER_SALES_VIEW;

        DBCommand cmd = context.createCommand();
        // Detect if Brand sold is the Dealer's Brand
        DBCompareExpr IS_DEALER_BRAND = DSV.DEALERSHIP_TYPE.in(DealershipType.B, DealershipType.F);
        // select
        cmd.select(DSV.SALE_YEAR, DSV.DEALER_NAME, DSV.DEALER_COUNTRY);
        cmd.select(DSV.SALE_COUNT.sum().as("TOTAL_SALES"));
        cmd.select(carSales.caseWhen(IS_DEALER_BRAND, DSV.TURNOVER, 0).sum().as("TURNOVER_BRAND"));
        cmd.select(DSV.TURNOVER.sum().as("TURNOVER_TOTAL"));
        // group
        cmd.groupBy(DSV.SALE_YEAR, DSV.DEALER_NAME, DSV.DEALER_COUNTRY);
        // order
        cmd.orderBy(DSV.SALE_YEAR, DSV.DEALER_NAME);

        // query and print result
        List<DataListEntry> list = context.getUtils().queryDataList(cmd);
        for (DataListEntry dle : list)
        {
            System.out.println(dle.toString());
        }
    }
    
    /**
     * <PRE>
     * Opens and returns a JDBC-Connection.
     * JDBC url, user and password for the connection are obtained from the SampleConfig bean
     * Please use the config.xml file to change connection params.
     * </PRE>
     */
    private static Connection getJDBCConnection()
    {
        // Establish a new database connection
        Connection conn = null;
        try
        {
            // Connect to the database
            String jdbcDriverClass = config.getJdbcClass();
            log.info("Creating JDBC-Driver of type \"{}\"", jdbcDriverClass);
            Class.forName(jdbcDriverClass).newInstance();

            log.info("Connecting to Database'" + config.getJdbcURL() + "' / User=" + config.getJdbcUser());
            conn = DriverManager.getConnection(config.getJdbcURL(), config.getJdbcUser(), config.getJdbcPwd());
            log.info("Connected successfully");
            // set the AutoCommit to false this session. You must commit
            // explicitly now
            conn.setAutoCommit(false);
            log.info("AutoCommit is " + conn.getAutoCommit());

        } catch (Exception e)
        {
            log.error("Failed to connect to '" + config.getJdbcURL() + "' / User=" + config.getJdbcUser());
            log.error(e.toString());
            throw new RuntimeException(e);
        }
        return conn;
    }

    /**
     * Creates an Empire-db DatabaseDriver for the given provider and applies dbms specific configuration 
     */
    private static DBMSHandler getDBMSHandler(String provider)
    {
        try
        {   // Get Driver Class Name
            String dbmsHandlerClass = config.getDbmsHandlerClass();
            if (StringUtils.isEmpty(dbmsHandlerClass))
                throw new RuntimeException("Configuration error: Element 'dbmsHandlerClass' not found in node 'properties-"+provider+"'");

            // Create dbms
            DBMSHandler dbms = (DBMSHandler) Class.forName(dbmsHandlerClass).newInstance();

            // Configure dbms
            config.readProperties(dbms, "properties-"+provider, "dbmsHandlerProperties");

            // done
            return dbms;
            
        } catch (Exception e)
        {   // catch any checked exception and forward it
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    /**
     * This method demonstrates how to add, modify and delete a database column.<BR>
     * This function demonstrates the use of the {@link DBMSHandler#getDDLScript(org.apache.empire.db.DDLActionType, org.apache.empire.db.DBObject, DBSQLScript)}<BR>
     * 
     */
    private void ddlDemo(String dealerName, String websiteDomain, String websiteUrl)
    {
        // create shortcuts
        DBMSHandler dbms = context.getDbms();
        CarSalesDB.Dealer DEALER = carSales.DEALER;

        // First, add a new column to the Table object
        DBTableColumn WEBSITE = DEALER.addColumn("WEBSITE", DataType.VARCHAR, 20, false);

        // Now create the corresponding DDL statement 
        log.info("Create new column named WEBSITE as varchar(20) for the DEALER table");
        DBSQLScript script = new DBSQLScript(context);
        dbms.getDDLScript(DDLActionType.CREATE, WEBSITE, script);
        script.executeAll();
        
        // Now load a record from that table and set the value for the website
        log.info("Set the value for the WEBSITE field for dealer \"{}\"", dealerName);
        DealerRecord rec = new DealerRecord(context);
        rec.read(DEALER.COMPANY_NAME.is(dealerName));
        rec.set (WEBSITE, websiteDomain);
        rec.update();
        
        // Now extend the size of the field from 40 to 80 characters
        log.info("Extend the size of column WEBSITE from 20 to 80 characters");
        WEBSITE.setSize(80); 
        script.clear();
        dbms.getDDLScript(DDLActionType.ALTER, WEBSITE, script);
        script.executeAll();

        // Now set a longer value for the record
        log.info("Change the value for the WEBSITE to a longer string \"{}\"", websiteUrl);
        rec.set(WEBSITE, websiteUrl);
        rec.update();

        // Finally, drop the column again
        log.info("Drop the WEBSITE column from the DEALER table:");
        script.clear();
        dbms.getDDLScript(DDLActionType.DROP, WEBSITE, script);
        script.executeAll();
    }

    /**
     * testTransactionCreate
     * @param context
     * @param idDep
     */
    private void transactionDemo()
    {
        // Set RollbackHandlingEnabled to false if you want to play with fire!
        // Note: You must set the RollbackHandling flag before creating a Record instance 
        context.setRollbackHandlingEnabled(true);

        // User a model record
        CarSalesDB.Model MODEL = carSales.MODEL;
        ModelRecord model = new ModelRecord(context);
        
        /*
         * Part 1
         */
        
        // Insert a new model
        log.info("Insert a new Model");
        model.create()
            .set(MODEL.WMI             , "WVW")  // = Volkswagen
            .set(MODEL.NAME            , "ID.4")
            .set(MODEL.SPECIFICATION   , "ID.4 Pro Performance 150 kW 77 kWh")
            .set(MODEL.TRIM            , "Pro")
            .set(MODEL.ENGINE_TYPE     , EngineType.E)
            .set(MODEL.ENGINE_POWER    , 204)
            .set(MODEL.FIRST_SALE      , LocalDate.of(2021, 04, 04));
        // State and timestampe before and after insert
        log.debug("Record state={}, key={}, Timestamp={}", model.getState(), model.getKey(), model.get(MODEL.UPDATE_TIMESTAMP));
        model.update();
        log.debug("Record state={}, key={}, Timestamp={}", model.getState(), model.getKey(), model.get(MODEL.UPDATE_TIMESTAMP));

        // Update even once again without committing
        model.set(MODEL.BASE_PRICE, 44915);
        model.update();
        log.debug("Record state={}, key={}, Timestamp={}", model.getState(), model.getKey(), model.get(MODEL.UPDATE_TIMESTAMP));
        
        // now, do the rollback
        log.info("Performing rollback");
        context.rollback();
        log.debug("Record state={}, key={}, Timestamp={}", model.getState(), model.getKey(), model.get(MODEL.UPDATE_TIMESTAMP));
        
        log.info("Update Model again, even though its not in the database because of the rollback, hence Model will be inserted again.");
        log.info("This will fail, if Rollback handling is not enabled!");
        try {
            model.set(MODEL.BASE_PRICE, 44900); // Only to mark the record as modified if RollbackHandling is disabled!
            model.update();
        } catch(RecordUpdateFailedException e) {
            log.error("Model update failed since rollback handling was not enabled for this context!", e);
            return; // the rest will fail too
        }
        log.debug("Record state={}, key={}, Timestamp={}", model.getState(), model.getKey(), model.get(MODEL.UPDATE_TIMESTAMP));

        /*
         * Part 2
         */
        
        // Delete the model
        model.delete();
        log.debug("Record state={}", model.getState());
        
        log.info("Performing rollback again");
        context.rollback();
        log.debug("Record state={}, key={}, Timestamp={}", model.getState(), model.getKey(), model.get(MODEL.UPDATE_TIMESTAMP));

        // Final update
        log.info("Update Model again, even though a delete was executed but was not committed.");
        log.info("This will once again lead to an insert as the insert was still not committed");
        log.info("This will also fail, if Rollback handling is not enabled!");
        try {
            model.update();
        } catch(RecordUpdateFailedException e) {
            log.error("Model update failed since rollback handling was not enabled for this context!", e);
            return; // the rest will fail too
        }
        log.debug("Record state={}, key={}, Timestamp={}", model.getState(), model.getKey(), model.get(MODEL.UPDATE_TIMESTAMP));
        
        // Finally commit
        log.info("Finally commit the transaction in order to finalize the Model insert");
        context.commit();
        log.debug("Record state={}, key={}, Timestamp={}", model.getState(), model.getKey(), model.get(MODEL.UPDATE_TIMESTAMP));
        
        /*
         * Part 3
         */
        
        // Update example with two consecutive updates
        log.info("Fist update will change the timestamp");
        model.set(MODEL.BASE_PRICE, 44000);
        model.update();
        log.debug("Record state={}, key={}, Timestamp={}", model.getState(), model.getKey(), model.get(MODEL.UPDATE_TIMESTAMP));
        
        log.info("Second update will change the timestamp again");
        model.set(MODEL.BASE_PRICE, 42660);
        model.update();
        log.debug("Record state={}, key={}, Timestamp={}", model.getState(), model.getKey(), model.get(MODEL.UPDATE_TIMESTAMP));
        
        log.info("Performing rollback for the previous two updates");
        context.rollback();
        log.debug("Record state={}, key={}, Timestamp={}", model.getState(), model.getKey(), model.get(MODEL.UPDATE_TIMESTAMP));
        
        log.info("Update Model again, even though the previous two updates have been rolled back");
        log.info("This will again fail, if Rollback handling is not enabled!");
        try {
            model.update();
        } catch(RecordUpdateFailedException e) {
            log.error("Model update failed since rollback handling was not enabled for this context!", e);
        }
        log.debug("Record state={}, key={}, Timestamp={}", model.getState(), model.getKey(), model.get(MODEL.UPDATE_TIMESTAMP));
        
        // But now delete the model again, so we can rerun the example later
        model.delete();
        context.commit();
    }
   
    /**
     * This function demonstrates cascaded deletes.
     * See DBRelation.setOnDeleteAction()
     *  
     * @param idEmployee the id of the employee to delete
     * @param idDepartment the id of the department to delete
     * @param conn the connection
     */
    private void cascadeDeleteDemo()
    {
        // Read a brand record
        BrandRecord brand = new BrandRecord(context);
        brand.read(carSales.BRAND.WMI.is("WVW")); // = Volkswagen
        
        // Read dealer record
        DealerRecord dealer = new DealerRecord(context);
        dealer.read(carSales.DEALER.COMPANY_NAME.is("Autorigoldi S.p.A."));

        try {
            // This delete is designed to succeed since cascaded deletes are enabled for this relation.
            // see DEALER_BRANDS and SALES foreign key definition in CarSalesDB:
            //   DEALER_ID = addForeignKey("DEALER_ID", db.DEALER,  true,  true );  // Delete Cascade on
            log.info("Deleting DEALER expecting to succeed since relationship to DEALER_BRANDS and SALES is cascading");
            dealer.delete();

            // This delete is designed to succeed since cascaded deletes are enabled for this relation.
            log.info("Deleting BRAND expecting to fail since relationship to DEALER_BRANDS and MODEL is NOT cascading");
            brand.delete();
            
        } catch(ConstraintViolationException e) {
            log.info("Delete operation failed due to existing depending records.");
        } catch(StatementFailedException e) {
            // Oops, the driver threw a SQLException instead
            log.info("Delete operation failed with SQLException {}", e.getMessage());
        } finally {
            // we don't really want to do this
            context.rollback();
        }
    }

    /**
     * This functions prints the results of a query which is performed using the supplied command
     * @param cmd the command to be used for performing the query
     * @param conn the connection
    private void printQueryResults(DBCommand cmd)
    {
        // Query Records and print output
        DBReader reader = new DBReader(context);
        try
        {   // Open Reader
            System.out.println("Running Query:");
            System.out.println(cmd.getSelect());
            reader.open(cmd);
            // Print column titles 
            System.out.println("---------------------------------");
            int count = reader.getFieldCount();
            for (int i=0; i<count; i++)
            {   // Print all column names
                DBColumnExpr c = reader.getColumn(i);
                if (i>0)
                    System.out.print("\t");
                System.out.print(c.getName());
            }
            // Print output
            System.out.println("");
            // Text-Output by iterating through all records.
            while (reader.moveNext())
            {
                for (int i=0; i<count; i++)
                {   // Print all field values
                    if (i>0)
                        System.out.print("\t");
                    // Check if conversion is necessary
                    DBColumnExpr c = reader.getColumn(i);
                    Options opt = c.getOptions();
                    if (opt!=null)
                    {   // Option Lookup
                        System.out.print(opt.get(reader.getValue(i)));
                    }
                    else
                    {   // Print String
                        System.out.print(reader.getString(i));
                    }
                }
                System.out.println("");
            }

        } finally
        {   // always close Reader
            reader.close();
        }
    }
     */
    
    
}
