content: /**
   * Copyright (C) 2012 the original author or authors.
   *
   * 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 co.jirm.orm.dao;
  
  import static co.jirm.core.util.JirmPrecondition.check;
  import static com.google.common.collect.Iterators.partition;
  import static com.google.common.collect.Iterators.peekingIterator;
  
  import java.util.Iterator;
  import java.util.LinkedHashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Map.Entry;
  
  import co.jirm.core.builder.QueryForNumber;
  import co.jirm.core.builder.TypedQueryFor;
  import co.jirm.core.execute.SqlExecutor;
  import co.jirm.core.util.ObjectMapUtils;
  import co.jirm.core.util.ObjectMapUtils.NestedKeyValue;
  import co.jirm.mapper.SqlObjectConfig;
  import co.jirm.mapper.copy.CopyBuilder;
  import co.jirm.mapper.definition.SqlObjectDefinition;
  import co.jirm.mapper.definition.SqlParameterDefinition;
  import co.jirm.orm.OrmConfig;
  import co.jirm.orm.builder.delete.DeleteBuilderFactory;
  import co.jirm.orm.builder.delete.DeleteRootClauseBuilder;
  import co.jirm.orm.builder.select.SelectBuilderFactory;
  import co.jirm.orm.builder.select.SelectRootClauseBuilder;
  import co.jirm.orm.builder.update.UpdateBuilderFactory;
  import co.jirm.orm.builder.update.UpdateObjectBuilder;
  import co.jirm.orm.builder.update.UpdateRootClauseBuilder;
  import co.jirm.orm.writer.SqlWriterStrategy;
  
  import com.google.common.base.Function;
  import com.google.common.base.Optional;
  import com.google.common.collect.ImmutableList;
  import com.google.common.collect.Iterators;
  import com.google.common.collect.Lists;
  import com.google.common.collect.PeekingIterator;
  
  
  public final class JirmDao<T> {
  
  	private final SqlExecutor sqlExecutor;
  	private final SqlObjectConfig config;
  	private final SqlObjectDefinition<T> definition;
  	private final SelectBuilderFactory<T> selectBuilderFactory;
  	private final UpdateBuilderFactory<T> updateBuilderFactory;
  	private final DeleteBuilderFactory<T> deleteBuilderFactory;
  	private final SqlWriterStrategy writerStrategy;
  	
  	private JirmDao(
  			SqlExecutor sqlExecutor, 
  			SqlObjectConfig config, 
  			SqlObjectDefinition<T> definition,
  			SqlWriterStrategy writerStrategy, 
  			SelectBuilderFactory<T> selectBuilderFactory,
  			UpdateBuilderFactory<T> updateBuilderFactory,
  			DeleteBuilderFactory<T> deleteBuilderFactory) {
  		super();
  		this.sqlExecutor = sqlExecutor;
  		this.config = config;
  		this.definition = definition;
  		this.writerStrategy = writerStrategy;
  		this.selectBuilderFactory = selectBuilderFactory;
  		this.updateBuilderFactory = updateBuilderFactory;
  		this.deleteBuilderFactory = deleteBuilderFactory;
  	}
  	
  	public static <T> JirmDao<T> newInstance(Class<T> type, OrmConfig config) {
  		SqlObjectDefinition<T> definition = config.getSqlObjectConfig().resolveObjectDefinition(type);
  		SelectBuilderFactory<T> selectBuilderFactory = SelectBuilderFactory.newInstance(definition, config);
  		UpdateBuilderFactory<T> updateBuilderFactory = UpdateBuilderFactory.newInstance(definition, config);
  		DeleteBuilderFactory<T> deleteBuilderFactory = DeleteBuilderFactory.newInstance(definition, config);
  		
  		return new JirmDao<T>(
  				config.getSqlExecutor(), 
  				config.getSqlObjectConfig(), 
  				definition, config.getSqlWriterStrategy(), 
  				selectBuilderFactory, updateBuilderFactory, deleteBuilderFactory);
  	}
  
  	private LinkedHashMap<String, Object> toLinkedHashMap(T t, boolean bulkInsert) {
  		LinkedHashMap<String, Object> m = config.getObjectMapper().convertObjectToSqlMap(t);
  		/*
  		 * Replace the complex objects with there ids.
  		 */
  		for(SqlParameterDefinition pd : definition.getManyToOneParameters().values()) {
  			if (pd.getObjectDefinition().isPresent() 
  					&& pd.getObjectDefinition().get().getObjectDefintion().idParameter().isPresent()) {
  				SqlParameterDefinition idDef = 
  						pd.getObjectDefinition().get().getObjectDefintion().idParameter().get();
  				NestedKeyValue<Object> nkv =  ObjectMapUtils.getNestedKeyValue(m, pd.getParameterName(), idDef.getParameterName());
  				if (nkv.isPresent()) {
  					/*
  					 * TODO: We only set it if the object is actually present. ie do you really want to set null?
  					 */
  					m.put(pd.getParameterName(), idDef.convertToSql(nkv.object));
  				}
  				else if (bulkInsert) {
  					//TODO default annotation perhaps here?
  					//http://stackoverflow.com/questions/197045/setting-default-values-for-columns-in-jpa
  					m.put(pd.getParameterName(), null);
  				}
  			}
  		}
  		for(Entry<String,Object> e : m.entrySet()) {
  			Optional<SqlParameterDefinition> d = definition.resolveParameter(e.getKey());
  			if (d.isPresent()) {
  				e.setValue(d.get().convertToSql(e.getValue()));
  			}
  		}
  		if (bulkInsert) {
  			LinkedHashMap<String, Object> copy = new LinkedHashMap<String, Object>(definition.getIdParameters().size());
  			/*
  			 * Order and the number of parameters is really important for bulk insert.
  			 */
  			for(SqlParameterDefinition pd : definition.getParameters().values()) {
  				check.state(m.containsKey(pd.getParameterName()), 
  						"Missing parameter for bulk insert: {}", pd.getParameterName());
  				Object o = m.get(pd.getParameterName());
  				copy.put(pd.getParameterName(), o);
  			}
  			m = copy;
  		}
  		return m;
  				
  	}
  	
  	public CopyBuilder<T> copyBuilder() {
  		return CopyBuilder.newInstance(definition.getObjectType(), config.getObjectMapper());
  	}
  	
  	protected SqlParameterDefinition idParameter() {
  		check.state(definition.idParameter().isPresent(), "No id parameter for : {}", 
  				definition.getObjectType());
  		return this.definition.idParameter().get();
  	}
  	
  	public SelectRootClauseBuilder<? extends TypedQueryFor<T>> select() {
  		return selectBuilderFactory.select();
  	}
  	
  	public SelectRootClauseBuilder<? extends QueryForNumber> count() {
  		return selectBuilderFactory.count();
  	}
  	
  	public UpdateRootClauseBuilder<Integer> update() {
  		return updateBuilderFactory.update();
  	}
  	
  	public DeleteRootClauseBuilder<Integer> delete() {
  		return deleteBuilderFactory.delete();
  	}
  	
  	public Optional<T> findOptionalById(Object id) {
  		return select().where()
  				.property(idParameter().getParameterName()).eq(id)
  				.query()
  				.forOptional();
  	}
  	
  	public T findById(Object id) {
  		return select().where()
  				.property(idParameter().getParameterName()).eq(id)
  				.query()
  				.forObject();
  	}
  	
  	public void insert(T t) {
  		LinkedHashMap<String, Object> m = toLinkedHashMap(t, false);
  		Iterator<Entry<String, Object>> it = m.entrySet().iterator();
  		/*
  		 * Remove the null values that are to be generated.
  		 */
  		while(it.hasNext()) {
  			Entry<String, Object> e = it.next();
  			Optional<SqlParameterDefinition> p = definition.resolveParameter(e.getKey());
  			if (p.isPresent() && p.get().isGenerated() && e.getValue() == null) {
  				it.remove();
  			}
  			else if (p.isPresent() && p.get().isVersion() && e.getValue() == null) {
  				e.setValue(0);
  			}
  		}
  		insert(m);
  	}
  	
  	public int deleteById(Object id) {
  		return deleteBuilderFactory
  			.delete()
  			.where().property(idParameter().getParameterName()).eq(id)
  			.execute();
  	}
  	
  	public UpdateObjectBuilder<T> update(T t) {
  		LinkedHashMap<String, Object> m = toLinkedHashMap(t, false);
  		return updateBuilderFactory.update(m);
  	}
  
  	public T reload(T t) {
  		LinkedHashMap<String, Object> m = toLinkedHashMap(t, false);
  		Optional<SqlParameterDefinition> id = definition.idParameter();
  		check.state(id.isPresent(), "No id definition");
  		Optional<Object> o = id.get().valueFrom(m);
  		return findById(o.get());
  	}
  	
  	public void insert(Map<String,Object> values) {
  		StringBuilder qb = new StringBuilder();
  		writerStrategy.insertStatement(qb, definition, values);
  		sqlExecutor.update(qb.toString(), writerStrategy.fillValues(definition, values).toArray());
  	}
  	
  	public void insert(Iterator<T> values, int batchSize) {
  		Iterator<Map<String,Object>> t = Iterators.transform(values, new Function<T, Map<String,Object>>() {
  			@Override
  			public Map<String, Object> apply(T input) {
  				return toLinkedHashMap(input, true);
  			}
  		});
  		insertMaps(t, batchSize);
  	}
  	
  	public void insertMaps(Iterator<Map<String,Object>> values, int batchSize) {
  		if (! values.hasNext() ) return;
  		PeekingIterator<Map<String,Object>> vs = peekingIterator(values);
  		Map<String,Object> first = vs.peek();
  		final String sql = writerStrategy.insertStatement(new StringBuilder(), definition, first).toString();
  		ImmutableList<String> keys = ImmutableList.copyOf(vs.peek().keySet());
  		Iterator<List<Map<String,Object>>> it = partition(vs, batchSize);
  
  		while (it.hasNext()) {
  			List<Map<String,Object>> batch = it.next();
  			final List<Object[]> batchValues = Lists.newArrayListWithExpectedSize(batch.size());
  			for (Map<String,Object> b : batch) {
  				ImmutableList<String> actualKeys = ImmutableList.copyOf(b.keySet());
  				check.state(actualKeys.equals(keys), "Keys don't match up to {} for {}", keys, actualKeys);
  				batchValues.add(writerStrategy.fillValues(definition, b).toArray());
  			}
  			/*
  			 * TODO this will keep making a prepared statementS.
  			 * Hopefully the JDBC driver has some caching for this.
  			 */
  			sqlExecutor.batchUpdate(sql, batchValues);
  		}
  		
  	}
  	
  	
  	public SelectBuilderFactory<T> getSelectBuilderFactory() {
  		return selectBuilderFactory;
  	}
  	
  	public UpdateBuilderFactory<T> getUpdateBuilderFactory() {
  		return updateBuilderFactory;
  	}
  }
  