/*
 * 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.beam.examples.cookbook;

import com.google.api.services.bigquery.model.TableRow;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.io.TextIO;
import org.apache.beam.sdk.io.gcp.bigquery.BigQueryIO;
import org.apache.beam.sdk.options.Description;
import org.apache.beam.sdk.options.PipelineOptions;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.options.Validation;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.ParDo;
import org.apache.beam.sdk.transforms.join.CoGbkResult;
import org.apache.beam.sdk.transforms.join.CoGroupByKey;
import org.apache.beam.sdk.transforms.join.KeyedPCollectionTuple;
import org.apache.beam.sdk.values.KV;
import org.apache.beam.sdk.values.PCollection;
import org.apache.beam.sdk.values.TupleTag;

/**
 * This example shows how to do a join on two collections.
 * It uses a sample of the GDELT 'world event' data (http://goo.gl/OB6oin), joining the event
 * 'action' country code against a table that maps country codes to country names.
 *
 * <p>Concepts: Join operation; multiple input sources.
 *
 * <p>To execute this pipeline locally, specify a local output file or output prefix on GCS:
 * <pre>{@code
 *   --output=[YOUR_LOCAL_FILE | gs://YOUR_OUTPUT_PREFIX]
 * }</pre>
 *
 * <p>To change the runner, specify:
 * <pre>{@code
 *   --runner=YOUR_SELECTED_RUNNER
 * }
 * </pre>
 * See examples/java/README.md for instructions about how to configure different runners.
 */
public class JoinExamples {

  // A 1000-row sample of the GDELT data here: gdelt-bq:full.events.
  private static final String GDELT_EVENTS_TABLE =
      "clouddataflow-readonly:samples.gdelt_sample";
  // A table that maps country codes to country names.
  private static final String COUNTRY_CODES =
      "gdelt-bq:full.crosswalk_geocountrycodetohuman";

  /**
   * Join two collections, using country code as the key.
   */
  static PCollection<String> joinEvents(PCollection<TableRow> eventsTable,
      PCollection<TableRow> countryCodes) throws Exception {

    final TupleTag<String> eventInfoTag = new TupleTag<>();
    final TupleTag<String> countryInfoTag = new TupleTag<>();

    // transform both input collections to tuple collections, where the keys are country
    // codes in both cases.
    PCollection<KV<String, String>> eventInfo = eventsTable.apply(
        ParDo.of(new ExtractEventDataFn()));
    PCollection<KV<String, String>> countryInfo = countryCodes.apply(
        ParDo.of(new ExtractCountryInfoFn()));

    // country code 'key' -> CGBKR (<event info>, <country name>)
    PCollection<KV<String, CoGbkResult>> kvpCollection =
        KeyedPCollectionTuple.of(eventInfoTag, eventInfo)
            .and(countryInfoTag, countryInfo)
            .apply(CoGroupByKey.create());

    // Process the CoGbkResult elements generated by the CoGroupByKey transform.
    // country code 'key' -> string of <event info>, <country name>
    PCollection<KV<String, String>> finalResultCollection =
      kvpCollection.apply("Process", ParDo.of(
        new DoFn<KV<String, CoGbkResult>, KV<String, String>>() {
          @ProcessElement
          public void processElement(ProcessContext c) {
            KV<String, CoGbkResult> e = c.element();
            String countryCode = e.getKey();
            String countryName = "none";
            countryName = e.getValue().getOnly(countryInfoTag);
            for (String eventInfo : c.element().getValue().getAll(eventInfoTag)) {
              // Generate a string that combines information from both collection values
              c.output(KV.of(countryCode, "Country name: " + countryName
                      + ", Event info: " + eventInfo));
            }
          }
      }));

    // write to GCS
    PCollection<String> formattedResults = finalResultCollection
        .apply("Format", ParDo.of(new DoFn<KV<String, String>, String>() {
          @ProcessElement
          public void processElement(ProcessContext c) {
            String outputstring = "Country code: " + c.element().getKey()
                + ", " + c.element().getValue();
            c.output(outputstring);
          }
        }));
    return formattedResults;
  }

  /**
   * Examines each row (event) in the input table. Output a KV with the key the country
   * code of the event, and the value a string encoding event information.
   */
  static class ExtractEventDataFn extends DoFn<TableRow, KV<String, String>> {
    @ProcessElement
    public void processElement(ProcessContext c) {
      TableRow row = c.element();
      String countryCode = (String) row.get("ActionGeo_CountryCode");
      String sqlDate = (String) row.get("SQLDATE");
      String actor1Name = (String) row.get("Actor1Name");
      String sourceUrl = (String) row.get("SOURCEURL");
      String eventInfo = "Date: " + sqlDate + ", Actor1: " + actor1Name + ", url: " + sourceUrl;
      c.output(KV.of(countryCode, eventInfo));
    }
  }


  /**
   * Examines each row (country info) in the input table. Output a KV with the key the country
   * code, and the value the country name.
   */
  static class ExtractCountryInfoFn extends DoFn<TableRow, KV<String, String>> {
    @ProcessElement
    public void processElement(ProcessContext c) {
      TableRow row = c.element();
      String countryCode = (String) row.get("FIPSCC");
      String countryName = (String) row.get("HumanName");
      c.output(KV.of(countryCode, countryName));
    }
  }


  /**
   * Options supported by {@link JoinExamples}.
   *
   * <p>Inherits standard configuration options.
   */
  private interface Options extends PipelineOptions {
    @Description("Path of the file to write to")
    @Validation.Required
    String getOutput();
    void setOutput(String value);
  }

  public static void main(String[] args) throws Exception {
    Options options = PipelineOptionsFactory.fromArgs(args).withValidation().as(Options.class);
    Pipeline p = Pipeline.create(options);
    // the following two 'applys' create multiple inputs to our pipeline, one for each
    // of our two input sources.
    PCollection<TableRow> eventsTable = p.apply(
        BigQueryIO.readTableRows().from(GDELT_EVENTS_TABLE));
    PCollection<TableRow> countryCodes = p.apply(
        BigQueryIO.readTableRows().from(COUNTRY_CODES));
    PCollection<String> formattedResults = joinEvents(eventsTable, countryCodes);
    formattedResults.apply(TextIO.write().to(options.getOutput()));
    p.run().waitUntilFinish();
  }

}
