
package cosmos.mapred;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import org.apache.accumulo.core.client.BatchScanner;
import org.apache.accumulo.core.client.BatchWriter;
import org.apache.accumulo.core.client.BatchWriterConfig;
import org.apache.accumulo.core.client.Connector;
import org.apache.accumulo.core.client.TableExistsException;
import org.apache.accumulo.core.client.ZooKeeperInstance;
import org.apache.accumulo.core.client.security.tokens.PasswordToken;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Mutation;
import org.apache.accumulo.core.data.Range;
import org.apache.accumulo.core.data.Value;
import org.apache.accumulo.core.security.Authorizations;
import org.apache.accumulo.core.security.ColumnVisibility;
import org.apache.hadoop.io.Text;
import org.mediawiki.xml.export_0.MediaWikiType;
import org.mediawiki.xml.export_0.PageType;

import com.google.common.base.Function;
import com.google.common.base.Stopwatch;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.protobuf.InvalidProtocolBufferException;

import cosmos.Cosmos;
import cosmos.impl.CosmosImpl;
import cosmos.mediawiki.MediawikiPage.Page;
import cosmos.mediawiki.MediawikiPage.Page.Revision;
import cosmos.mediawiki.MediawikiPage.Page.Revision.Contributor;
import cosmos.options.Defaults;
import cosmos.options.Index;
import cosmos.records.impl.MultimapRecord;
import cosmos.records.values.RecordValue;
import cosmos.results.CloseableIterable;
import cosmos.results.Column;
import cosmos.results.integration.CosmosIntegrationSetup;
import cosmos.store.Store;
import cosmos.util.IdentitySet;

/**
 * 
 */
public class MediawikiQueries {
  public static final boolean preloadData = false;
  
  public static final String TIMINGS = "[TIMINGS] ";
  public static final int MAX_SIZE = 8000;
  
  // MAX_OFFSET is a little misleading because the max pageID is 33928886
  // Don't have contiguous pageIDs
  public static final int MAX_OFFSET = 11845576 - MAX_SIZE;
  
  public static final int MAX_ROW = 999999999;
  
  public static final ColumnVisibility cv = new ColumnVisibility("en");
  
  public static final Column PAGE_ID = Column.create("PAGE_ID"), REVISION_ID = Column.create("REVISION_ID"), REVISION_TIMESTAMP = Column
      .create("REVISION_TIMESTAMP"), CONTRIBUTOR_USERNAME = Column.create("CONTRIBUTOR_USERNAME"), CONTRIBUTOR_ID = Column.create("CONTRIBUTOR_ID");
  
  public static void logTiming(long numResults, long duration, String action) {
    System.err.println(TIMINGS + numResults + " " + duration + " " + action);
  }
  
  public static MultimapRecord pagesToQueryResult(Page p) {
    HashMultimap<Column,RecordValue<?>> data = HashMultimap.create();
    
    String pageId = Long.toString(p.getId());
    
    data.put(PAGE_ID, RecordValue.create(pageId, cv));
    
    Revision r = p.getRevision();
    if (null != r) {
      data.put(REVISION_ID, RecordValue.create(Long.toString(r.getId()), cv));
      data.put(REVISION_TIMESTAMP, RecordValue.create(r.getTimestamp(), cv));
      
      Contributor c = r.getContributor();
      if (null != c) {
        if (null != c.getUsername()) {
          data.put(CONTRIBUTOR_USERNAME, RecordValue.create(c.getUsername(), cv));
        }
        
        if (0l != c.getId()) {
          data.put(CONTRIBUTOR_ID, RecordValue.create(Long.toString(c.getId()), cv));
        }
      }
    }
    
    return new MultimapRecord(data, pageId, cv);
  }
  
  protected final Connector con;
  protected final Cosmos sorts;
  
  public MediawikiQueries() throws Exception {
    ZooKeeperInstance zk = new ZooKeeperInstance("accumulo", "localhost");
    this.con = zk.getConnector("root", new PasswordToken("secret"));
    
    this.sorts = new CosmosImpl("localhost");
  }
  
  public void run(int numIterations) throws Exception {
    final Random offsetR = new Random(), cardinalityR = new Random();
    
    int iters = 0;
    
    while (iters < numIterations) {
      Store id = Store.create(this.con, this.con.securityOperations().getUserAuthorizations(this.con.whoami()), IdentitySet.<Index> create());
      
      int offset = offsetR.nextInt(MAX_OFFSET);
      int numRecords = cardinalityR.nextInt(MAX_SIZE) + 1;
      
      BatchScanner bs = this.con.createBatchScanner("sortswiki", new Authorizations(), 4);
      
      bs.setRanges(Collections.singleton(new Range(Integer.toString(offset), Integer.toString(MAX_ROW))));
      
      Iterable<Entry<Key,Value>> inputIterable = Iterables.limit(bs, numRecords);
      
      this.sorts.register(id);
      
      System.out.println(Thread.currentThread().getName() + ": " + id.uuid() + " - Iteration " + iters);
      long recordsReturned = 0l;
      Function<Entry<Key,Value>,MultimapRecord> func = new Function<Entry<Key,Value>,MultimapRecord>() {
        @Override
        public MultimapRecord apply(Entry<Key,Value> input) {
          Page p;
          try {
            p = Page.parseFrom(input.getValue().get());
          } catch (InvalidProtocolBufferException e) {
            throw new RuntimeException(e);
          }
          return pagesToQueryResult(p);
        }
      };
      
      Map<Column,Long> counts = Maps.newHashMap();
      ArrayList<MultimapRecord> tformSource = Lists.newArrayListWithCapacity(20000);
      
      Stopwatch sw = new Stopwatch();
      Stopwatch tformSw = new Stopwatch();
      
      for (Entry<Key,Value> input : inputIterable) {
        tformSw.start();
        
        MultimapRecord r = func.apply(input);
        tformSource.add(r);
        
        tformSw.stop();
        
        loadCountsForRecord(counts, r);
        recordsReturned++;
      }
      
      sw.start();
      this.sorts.addResults(id, tformSource);
      sw.stop();
      
      long actualNumResults = tformSource.size();
      
      System.out.println(Thread.currentThread().getName() + ": Took " + tformSw + " transforming and " + sw + " to store " + recordsReturned + " records");
      logTiming(actualNumResults, tformSw.elapsed(TimeUnit.MILLISECONDS), "transformInput");
      logTiming(actualNumResults, sw.elapsed(TimeUnit.MILLISECONDS), "ingest");

      bs.close();
      
      Random r = new Random();
      int max = r.nextInt(10) + 1;
      
      // Run a bunch of queries
      for (int count = 0; count < max; count++) {
        long resultCount;
        String name;
        int i = r.nextInt(9);
        
        if (0 == i) {
          resultCount = docIdFetch(id, counts, actualNumResults);
          name = "docIdFetch";
        } else if (1 == i) {
          resultCount = columnFetch(id, REVISION_ID, counts, actualNumResults);
          name = "revisionIdFetch";
        } else if (2 == i) {
          resultCount = columnFetch(id, PAGE_ID, counts, actualNumResults);
          name = "pageIdFetch";
        } else if (3 == i) {
          groupBy(id, REVISION_ID, counts, actualNumResults);
          // no sense to verify here
          resultCount = recordsReturned;
          name = "groupByRevisionId";
        } else if (4 == i) {
          groupBy(id, PAGE_ID, counts, actualNumResults);
          // no sense to verify here
          resultCount = recordsReturned;
          name = "groupByRevisionId";
        } else if (5 == i) {
          resultCount = columnFetch(id, CONTRIBUTOR_USERNAME, counts, actualNumResults);
          name = "contributorUsernameFetch";
        } else if (6 == i) {
          groupBy(id, CONTRIBUTOR_USERNAME, counts, actualNumResults);
          // no sense to verify here
          resultCount = recordsReturned;
          name = "groupByContributorUsername";
        } else if (7 == i) {
          resultCount = columnFetch(id, CONTRIBUTOR_ID, counts, actualNumResults);
          name = "contributorIdFetch";
        } else {//if (8 == i) {
          groupBy(id, CONTRIBUTOR_ID, counts, actualNumResults);
          // no sense to verify here
          resultCount = recordsReturned;
          name = "groupByContributorID";
        }
      }
      System.out.println(Thread.currentThread().getName() + ": not deleting " + id );
      // Delete the results
      sw = new Stopwatch();
      
      sw.start();
      
      this.sorts.delete(id);
      sw.stop();
      
      System.out.println(Thread.currentThread().getName() + ": Took " + sw.toString() + " to delete results");
      logTiming(actualNumResults, sw.elapsed(TimeUnit.MILLISECONDS), "deleteResults");
      
      iters++;
    }
    
    this.sorts.close();
  }
  
  public void loadCountsForRecord(Map<Column,Long> counts, MultimapRecord r) {
	  for (Entry<Column,RecordValue<?>> entry : r.columnValues()) {
		  Column c = entry.getKey();
		  if (counts.containsKey(c)) {
			  counts.put(c, counts.get(c)+1);
		  } else {
			  counts.put(c, 1l);
		  }
	  }
  }
  
  public long docIdFetch(Store id, Map<Column,Long> counts, long totalResults) throws Exception {
    Stopwatch sw = new Stopwatch();
    
    // This is dumb, I didn't pad the docids...
    String prev = "!";
    long resultCount = 0l;
    sw.start();
    
    final CloseableIterable<MultimapRecord> results = this.sorts.fetch(id, Index.define(Defaults.DOCID_FIELD_NAME));
    
    for (MultimapRecord r : results) {
      sw.stop();
      
      resultCount++;
      
      String current = r.docId();
      if (prev.compareTo(current) > 0) {
        System.out.println("WOAH, got " + current + " docid which was greater than the previous " + prev);
        results.close();
        System.exit(1);
      }
      
      prev = current;
      
      sw.start();
    }
    
    sw.stop();
    
    System.out.println(Thread.currentThread().getName() + ": docIdFetch - Took " + sw.toString() + " to fetch results");
    logTiming(totalResults, sw.elapsed(TimeUnit.MILLISECONDS), "docIdFetch");
    
    results.close();
    
    return resultCount;
  }
  
  public long columnFetch(Store id, Column colToFetch, Map<Column,Long> counts, long totalResults) throws Exception {
    Stopwatch sw = new Stopwatch();
    String prev = null;
    String lastDocId = null;
    long resultCount = 0l;
    
    sw.start();
    final CloseableIterable<MultimapRecord> results = this.sorts.fetch(id, Index.define(colToFetch));
    Iterator<MultimapRecord> resultsIter = results.iterator();
    
    for (; resultsIter.hasNext();) {
      MultimapRecord r = resultsIter.next();
      
      sw.stop();
      resultCount++;
      
      Collection<RecordValue<?>> values = r.get(colToFetch);
      
      TreeSet<RecordValue<?>> sortedValues = Sets.newTreeSet(values);
      
      if (null == prev) {
        prev = sortedValues.first().value().toString();
      } else {
        boolean plausible = false;
        Iterator<RecordValue<?>> iter = sortedValues.iterator();
        for (; !plausible && iter.hasNext();) {
          String val = iter.next().value().toString();
          if (prev.compareTo(val) <= 0) {
            plausible = true;
          }
        }
        
        if (!plausible) {
          System.out.println(Thread.currentThread().getName() + ": " + colToFetch + " - " + lastDocId + " shouldn't have come before " + r.docId());
          System.out.println(prev + " compared to " + sortedValues);
          results.close();
          System.exit(1);
        }
      }
      
      lastDocId = r.docId();
      
      sw.start();
    }
    
    sw.stop();
    
    System.out.println(Thread.currentThread().getName() + ": " + colToFetch + " - Took " + sw.toString() + " to fetch results");
    logTiming(totalResults, sw.elapsed(TimeUnit.MILLISECONDS), "fetch:" + colToFetch);

    results.close();
    
    long expected = counts.containsKey(colToFetch) ? counts.get(colToFetch) : -1;
    
    if (resultCount != expected) {
      System.out.println(Thread.currentThread().getName() + " " + colToFetch + ": Expected to get " + expected + " records but got " + resultCount);
      System.exit(1);
    }
    
    return resultCount;
  }
  
  public void groupBy(Store id, Column colToFetch, Map<Column,Long> columnCounts, long totalResults) throws Exception {
    Stopwatch sw = new Stopwatch();
    
    sw.start();
    final CloseableIterable<Entry<RecordValue<?>,Long>> results = this.sorts.groupResults(id, colToFetch);
    TreeMap<RecordValue<?>,Long> counts = Maps.newTreeMap();
    
    for (Entry<RecordValue<?>,Long> entry : results) {
      counts.put(entry.getKey(), entry.getValue());
    }
    
    results.close();
    sw.stop();
    
    System.out.println(Thread.currentThread().getName() + ": " + colToFetch + " - Took " + sw.toString() + " to group results");
    logTiming(totalResults, sw.elapsed(TimeUnit.MILLISECONDS), "groupBy:" + colToFetch);

//    System.out.println(counts);
    
    final CloseableIterable<MultimapRecord> verifyResults = this.sorts.fetch(id, Index.define(colToFetch));
    TreeMap<RecordValue<?>,Long> records = Maps.newTreeMap();
    for (MultimapRecord r : verifyResults) {
      if (r.containsKey(colToFetch)) {
        for (RecordValue<?> val : r.get(colToFetch)) {
          if (records.containsKey(val)) {
            records.put(val, records.get(val) + 1);
          } else {
            records.put(val, 1l);
          }
        }
      }
    }

    verifyResults.close();
    
    if (counts.size() != records.size()) {
      System.out.println(Thread.currentThread().getName() + ": " + colToFetch + " - Expected " + records.size() + " groups but found " + counts.size());
      System.exit(1);
    }
    
    Set<RecordValue<?>> countKeys= counts.keySet(), recordKeys = records.keySet();
    for (RecordValue<?> k : countKeys) {
      if (!recordKeys.contains(k)) {
        System.out.println(Thread.currentThread().getName() + ": " + colToFetch + " - Expected to have count for " + k); 
        System.exit(1); 
      }
      
      Long actual = counts.get(k), expected = records.get(k);
      
      if (!actual.equals(expected)) {
        System.out.println(Thread.currentThread().getName() + ": " + colToFetch + " - Expected " + expected + " value(s) but found " + actual + " value(s) for " + k.value());
        System.exit(1);
      }
    }
  }
  
  public static Runnable runQueries(final int numQueries) {
    return new Runnable() {
      public void run() {
        try {
          (new MediawikiQueries()).run(numQueries);
        } catch (Exception e) {
        	e.printStackTrace();
          throw new RuntimeException(e);
        }
      }
    };
  }
  
  public static void main(String[] args) throws Exception {
    if (preloadData) {
      CosmosIntegrationSetup.initializeJaxb();
      MediawikiQueries queries = new MediawikiQueries();
      MediawikiMapper mapper = new MediawikiMapper();
      mapper.setup(null);
      
      List<MediaWikiType> results = Lists.newArrayList(CosmosIntegrationSetup.getWiki1(), CosmosIntegrationSetup.getWiki2(), CosmosIntegrationSetup.getWiki3(), CosmosIntegrationSetup.getWiki4(), CosmosIntegrationSetup.getWiki5());
      
      try {
        queries.con.tableOperations().create("sortswiki");
      } catch (TableExistsException e) {
        
      }
      BatchWriter bw = queries.con.createBatchWriter("sortswiki", new BatchWriterConfig());
      int i = 0;
      for (MediaWikiType wiki : results) {
        for (PageType pageType : wiki.getPage()) {
          Page page = mapper.pageTypeToPage(pageType);
          Value v = new Value(page.toByteArray());
          
          Mutation m = new Mutation(Integer.toString(i));
          m.put(new Text(), new Text(), v);
          bw.addMutation(m);
          i++;
        }
        bw.flush();
      }
      
      bw.close();
    }
    

    ExecutorService runner = Executors.newFixedThreadPool(3);
    for (int i = 0; i < 4; i++) {
      runner.execute(runQueries(200));
    }
    
    runner.shutdown();
    runner.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS);
  }
}
