text:
package org.mitre.synthea.export;

import static org.mitre.synthea.export.ExportHelper.dateFromTimestamp;
import static org.mitre.synthea.export.ExportHelper.getSystemFromURI;
import static org.mitre.synthea.export.ExportHelper.iso8601Timestamp;

import com.google.common.collect.Table;
import com.google.gson.JsonObject;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.GregorianCalendar;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.apache.commons.io.output.NullOutputStream;
import org.apache.commons.lang3.StringUtils;
import org.mitre.synthea.helpers.Config;
import org.mitre.synthea.helpers.RandomCodeGenerator;
import org.mitre.synthea.helpers.RandomNumberGenerator;
import org.mitre.synthea.helpers.Utilities;
import org.mitre.synthea.modules.QualityOfLifeModule;
import org.mitre.synthea.world.agents.Clinician;
import org.mitre.synthea.world.agents.Payer;
import org.mitre.synthea.world.agents.PayerManager;
import org.mitre.synthea.world.agents.Person;
import org.mitre.synthea.world.agents.Provider;
import org.mitre.synthea.world.concepts.Claim;
import org.mitre.synthea.world.concepts.HealthRecord;
import org.mitre.synthea.world.concepts.HealthRecord.CarePlan;
import org.mitre.synthea.world.concepts.HealthRecord.Code;
import org.mitre.synthea.world.concepts.HealthRecord.Device;
import org.mitre.synthea.world.concepts.HealthRecord.Encounter;
import org.mitre.synthea.world.concepts.HealthRecord.EncounterType;
import org.mitre.synthea.world.concepts.HealthRecord.Entry;
import org.mitre.synthea.world.concepts.HealthRecord.ImagingStudy;
import org.mitre.synthea.world.concepts.HealthRecord.Medication;
import org.mitre.synthea.world.concepts.HealthRecord.Observation;
import org.mitre.synthea.world.concepts.HealthRecord.Procedure;
import org.mitre.synthea.world.concepts.HealthRecord.Supply;
import org.mitre.synthea.world.concepts.healthinsurance.CoverageRecord.PlanRecord;

/**
 * Researchers have requested a simple table-based format that could easily be
 * imported into any database for analysis. Unlike other formats which export a
 * single record per patient, this format generates 9 total files, and adds
 * lines to each based on the clinical events for each patient. These files are
 * intended to be analogous to database tables, with the patient UUID being a
 * foreign key. Files include: patients.csv, encounters.csv, allergies.csv,
 * medications.csv, conditions.csv, careplans.csv, observations.csv,
 * procedures.csv, and immunizations.csv.
 */
public class CSVExporter {
  /**
   * Writer for patients.csv.
   */
  private OutputStreamWriter patients;
  /**
   * Writer for allergies.csv.
   */
  private OutputStreamWriter allergies;
  /**
   * Writer for medications.csv.
   */
  private OutputStreamWriter medications;
  /**
   * Writer for conditions.csv.
   */
  private OutputStreamWriter conditions;
  /**
   * Writer for careplans.csv.
   */
  private OutputStreamWriter careplans;
  /**
   * Writer for observations.csv.
   */
  private OutputStreamWriter observations;
  /**
   * Writer for procedures.csv.
   */
  private OutputStreamWriter procedures;
  /**
   * Writer for immunizations.csv.
   */
  private OutputStreamWriter immunizations;
  /**
   * Writer for encounters.csv.
   */
  private OutputStreamWriter encounters;
  /**
   * Writer for imaging_studies.csv
   */
  private OutputStreamWriter imagingStudies;
  /**
   * Writer for devices.csv
   */
  private OutputStreamWriter devices;
  /**
   * Writer for supplies.csv
   */
  private OutputStreamWriter supplies;

  /**
   * Writer for organizations.csv
   */
  private OutputStreamWriter organizations;
  /**
   * Writer for providers.csv
   */
  private OutputStreamWriter providers;

  /**
   * Writer for payers.csv
   */
  private OutputStreamWriter payers;
  /**
   * Writer for payerTransitions.csv
   */
  private OutputStreamWriter payerTransitions;
  /**
   * Writer for claims.csv
   */
  private OutputStreamWriter claims;
  /**
   * Writer for claims_transactions.csv
   */
  private OutputStreamWriter claimsTransactions;
  /**
   * Writer for patient_expenses.csv
   */
  private OutputStreamWriter patientExpenses;

  /**
   * Charset for specifying the character set of the output files.
   */
  private Charset charset = Charset.forName(Config.get("exporter.encoding", "UTF-8"));

  /**
   * System-dependent string for a line break. (\n on Mac, *nix, \r\n on Windows)
   */
  private static final String NEWLINE = System.lineSeparator();

  /**
   * Thread-safe monotonically increasing transactionId.
   */
  private AtomicLong transactionId;

  /**
   * Constructor for the CSVExporter - initialize the specified files and store
   * the writers in fields.
   */
  private CSVExporter() {
    init();
  }

  void init() {
    try {
      File output = Exporter.getOutputFolder("csv", null);
      output.mkdirs();
      Path outputDirectory = output.toPath();

      if (Config.getAsBoolean("exporter.csv.folder_per_run")) {
        // we want a folder per run, so name it based on the timestamp
        String timestamp = ExportHelper.iso8601Timestamp(System.currentTimeMillis());
        String subfolderName = timestamp.replaceAll("\\W+", "_"); // make sure it's filename-safe
        outputDirectory = outputDirectory.resolve(subfolderName);
        outputDirectory.toFile().mkdirs();
      }

      String includedFilesStr = Config.get("exporter.csv.included_files", "").trim();
      String excludedFilesStr = Config.get("exporter.csv.excluded_files", "").trim();

      List<String> includedFiles = Collections.emptyList();
      List<String> excludedFiles = Collections.emptyList();

      if (!includedFilesStr.isEmpty() && !excludedFilesStr.isEmpty()) {
        System.err.println(
            "CSV exporter: Included and Excluded file settings are both set -- ignoring both");
      } else {
        if (!includedFilesStr.isEmpty()) {
          includedFiles = propStringToList(includedFilesStr);

          if (!includedFiles.contains("patients.csv")) {
            System.err.println("WARNING! CSV exporter is set to not include patients.csv!");
            System.err.println("This is probably not what you want!");
          }

        } else {
          excludedFiles = propStringToList(excludedFilesStr);
        }
      }

      boolean append = Config.getAsBoolean("exporter.csv.append_mode");
      patients = getWriter(outputDirectory, "patients.csv", append, includedFiles, excludedFiles);

      allergies = getWriter(outputDirectory, "allergies.csv", append, includedFiles, excludedFiles);

      medications = getWriter(outputDirectory, "medications.csv", append, includedFiles,
          excludedFiles);

      conditions = getWriter(outputDirectory, "conditions.csv", append, includedFiles,
          excludedFiles);

      careplans = getWriter(outputDirectory, "careplans.csv", append, includedFiles, excludedFiles);

      observations = getWriter(outputDirectory, "observations.csv", append, includedFiles,
          excludedFiles);

      procedures = getWriter(outputDirectory, "procedures.csv", append, includedFiles,
          excludedFiles);

      immunizations = getWriter(outputDirectory, "immunizations.csv", append, includedFiles,
          excludedFiles);

      encounters = getWriter(outputDirectory, "encounters.csv", append, includedFiles,
          excludedFiles);

      imagingStudies = getWriter(outputDirectory, "imaging_studies.csv", append, includedFiles,
          excludedFiles);

      devices = getWriter(outputDirectory, "devices.csv", append, includedFiles, excludedFiles);

      supplies = getWriter(outputDirectory, "supplies.csv", append, includedFiles, excludedFiles);

      organizations = getWriter(outputDirectory, "organizations.csv", append, includedFiles,
          excludedFiles);

      providers = getWriter(outputDirectory, "providers.csv", append, includedFiles, excludedFiles);

      payers = getWriter(outputDirectory, "payers.csv", append, includedFiles, excludedFiles);

      payerTransitions = getWriter(outputDirectory, "payer_transitions.csv", append, includedFiles,
          excludedFiles);

      claims = getWriter(outputDirectory, "claims.csv", append, includedFiles, excludedFiles);

      claimsTransactions = getWriter(outputDirectory, "claims_transactions.csv", append,
          includedFiles, excludedFiles);

      patientExpenses = getWriter(outputDirectory, "patient_expenses.csv",
          append, includedFiles, excludedFiles);

      if (!append) {
        writeCSVHeaders();
      }
    } catch (IOException e) {
      // wrap the exception in a runtime exception.
      // the singleton pattern below doesn't work if the constructor can throw
      // and if these do throw ioexceptions there's nothing we can do anyway
      throw new RuntimeException(e);
    }

    this.transactionId = new AtomicLong();
  }

  /**
   * Helper function to convert a list of files directly from synthea.properties to filenames.
   * @param fileListString String directly from Config, ex "patients.csv,conditions , procedures"
   * @return normalized list of filenames as strings
   */
  private static List<String> propStringToList(String fileListString) {
    List<String> files = Arrays.asList(fileListString.split(","));
    // normalize filenames -- trim, lowercase, add .csv if not included
    files = files.stream().map(f -> {
      f = f.trim().toLowerCase();
      if (!f.endsWith(".csv")) {
        f = f + ".csv";
      }
      return f;
    }).collect(Collectors.toList());

    return files;
  }

  /**
   * Write the headers to each of the CSV files.
   * @throws IOException if any IO error occurs
   */
  private void writeCSVHeaders() throws IOException {
    patients.write("Id,BIRTHDATE,DEATHDATE,SSN,DRIVERS,PASSPORT,"
        + "PREFIX,FIRST,LAST,SUFFIX,MAIDEN,MARITAL,RACE,ETHNICITY,GENDER,BIRTHPLACE,"
        + "ADDRESS,CITY,STATE,COUNTY,FIPS,ZIP,LAT,LON,"
        + "HEALTHCARE_EXPENSES,HEALTHCARE_COVERAGE,INCOME");
    patients.write(NEWLINE);
    allergies.write("START,STOP,PATIENT,ENCOUNTER,CODE,SYSTEM,DESCRIPTION,TYPE,CATEGORY,"
        + "REACTION1,DESCRIPTION1,SEVERITY1,REACTION2,DESCRIPTION2,SEVERITY2");
    allergies.write(NEWLINE);
    medications.write(
        "START,STOP,PATIENT,PAYER,ENCOUNTER,CODE,DESCRIPTION,BASE_COST,PAYER_COVERAGE,DISPENSES,"
        + "TOTALCOST,REASONCODE,REASONDESCRIPTION");
    medications.write(NEWLINE);
    conditions.write("START,STOP,PATIENT,ENCOUNTER,CODE,DESCRIPTION");
    conditions.write(NEWLINE);
    careplans.write(
        "Id,START,STOP,PATIENT,ENCOUNTER,CODE,DESCRIPTION,REASONCODE,REASONDESCRIPTION");
    careplans.write(NEWLINE);
    observations.write("DATE,PATIENT,ENCOUNTER,CATEGORY,CODE,DESCRIPTION,VALUE,UNITS,TYPE");
    observations.write(NEWLINE);
    procedures.write("START,STOP,PATIENT,ENCOUNTER,CODE,DESCRIPTION,BASE_COST,"
        + "REASONCODE,REASONDESCRIPTION");
    procedures.write(NEWLINE);
    immunizations.write("DATE,PATIENT,ENCOUNTER,CODE,DESCRIPTION,BASE_COST");
    immunizations.write(NEWLINE);
    encounters.write(
        "Id,START,STOP,PATIENT,ORGANIZATION,PROVIDER,PAYER,ENCOUNTERCLASS,CODE,DESCRIPTION,"
        + "BASE_ENCOUNTER_COST,TOTAL_CLAIM_COST,PAYER_COVERAGE,REASONCODE,REASONDESCRIPTION");
    encounters.write(NEWLINE);
    imagingStudies.write("Id,DATE,PATIENT,ENCOUNTER,SERIES_UID,BODYSITE_CODE,BODYSITE_DESCRIPTION,"
        + "MODALITY_CODE,MODALITY_DESCRIPTION,INSTANCE_UID,SOP_CODE,SOP_DESCRIPTION,"
        + "PROCEDURE_CODE");
    imagingStudies.write(NEWLINE);
    devices.write("START,STOP,PATIENT,ENCOUNTER,CODE,DESCRIPTION,UDI");
    devices.write(NEWLINE);
    supplies.write("DATE,PATIENT,ENCOUNTER,CODE,DESCRIPTION,QUANTITY");
    supplies.write(NEWLINE);

    organizations.write("Id,NAME,ADDRESS,CITY,STATE,ZIP,LAT,LON,PHONE,REVENUE,UTILIZATION");
    organizations.write(NEWLINE);
    providers.write("Id,ORGANIZATION,NAME,GENDER,SPECIALITY,ADDRESS,CITY,STATE,ZIP,LAT,LON,"
        + "ENCOUNTERS,PROCEDURES");
    providers.write(NEWLINE);
    payers.write("Id,NAME,OWNERSHIP,ADDRESS,CITY,STATE_HEADQUARTERED,ZIP,PHONE,AMOUNT_COVERED,"
        + "AMOUNT_UNCOVERED,REVENUE,COVERED_ENCOUNTERS,UNCOVERED_ENCOUNTERS,COVERED_MEDICATIONS,"
        + "UNCOVERED_MEDICATIONS,COVERED_PROCEDURES,UNCOVERED_PROCEDURES,"
        + "COVERED_IMMUNIZATIONS,UNCOVERED_IMMUNIZATIONS,"
        + "UNIQUE_CUSTOMERS,QOLS_AVG,MEMBER_MONTHS");
    payers.write(NEWLINE);
    payerTransitions.write("PATIENT,MEMBERID,START_DATE,END_DATE,PAYER,SECONDARY_PAYER,"
        + "PLAN_OWNERSHIP,OWNER_NAME");
    payerTransitions.write(NEWLINE);
    claims.write("Id,PATIENTID,PROVIDERID,PRIMARYPATIENTINSURANCEID,SECONDARYPATIENTINSURANCEID,"
        + "DEPARTMENTID,PATIENTDEPARTMENTID,DIAGNOSIS1,DIAGNOSIS2,DIAGNOSIS3,DIAGNOSIS4,"
        + "DIAGNOSIS5,DIAGNOSIS6,DIAGNOSIS7,DIAGNOSIS8,REFERRINGPROVIDERID,APPOINTMENTID,"
        + "CURRENTILLNESSDATE,SERVICEDATE,SUPERVISINGPROVIDERID,STATUS1,STATUS2,STATUSP,"
        + "OUTSTANDING1,OUTSTANDING2,OUTSTANDINGP,LASTBILLEDDATE1,LASTBILLEDDATE2,LASTBILLEDDATEP,"
        + "HEALTHCARECLAIMTYPEID1,HEALTHCARECLAIMTYPEID2");
    claims.write(NEWLINE);
    claimsTransactions.write("ID,CLAIMID,CHARGEID,PATIENTID,TYPE,AMOUNT,METHOD,FROMDATE,TODATE,"
        + "PLACEOFSERVICE,PROCEDURECODE,MODIFIER1,MODIFIER2,DIAGNOSISREF1,DIAGNOSISREF2,"
        + "DIAGNOSISREF3,DIAGNOSISREF4,UNITS,DEPARTMENTID,NOTES,UNITAMOUNT,TRANSFEROUTID,"
        + "TRANSFERTYPE,PAYMENTS,ADJUSTMENTS,TRANSFERS,OUTSTANDING,APPOINTMENTID,LINENOTE,"
        + "PATIENTINSURANCEID,FEESCHEDULEID,PROVIDERID,SUPERVISINGPROVIDERID");
    claimsTransactions.write(NEWLINE);
    patientExpenses.write("PATIENT_ID,YEAR,PAYER_ID,"
        + "HEALTHCARE_EXPENSES,INSURANCE_COSTS,COVERED_COSTS");
    patientExpenses.write(NEWLINE);
  }

  /**
   * Thread safe singleton pattern adopted from
   * https://stackoverflow.com/questions/7048198/thread-safe-singletons-in-java
   */
  private static class SingletonHolder {
    /**
     * Singleton instance of the CSVExporter.
     */
    private static final CSVExporter instance = new CSVExporter();
  }

  /**
   * Get the current instance of the CSVExporter.
   *
   * @return the current instance of the CSVExporter.
   */
  public static CSVExporter getInstance() {
    return SingletonHolder.instance;
  }

  /**
   * Export the organizations.csv and providers.csv files. This method should be
   * called once after all the Patient records have been exported using the
   * export(Person,long) method.
   *
   * @throws IOException if any IO errors occur.
   */
  public void exportOrganizationsAndProviders() throws IOException {
    for (Provider org : Provider.getProviderList()) {
      // Check utilization for hospital before we export
      Table<Integer, String, AtomicInteger> utilization = org.getUtilization();
      int totalEncounters =
          utilization.column(Provider.ENCOUNTERS).values().stream().mapToInt(ai -> ai.get()).sum();
      if (totalEncounters > 0) {
        organization(org, totalEncounters);
        Map<String, ArrayList<Clinician>> providers = org.clinicianMap;
        for (String speciality : providers.keySet()) {
          ArrayList<Clinician> clinicians = providers.get(speciality);
          for (Clinician clinician : clinicians) {
            provider(clinician, org.getResourceID());
          }
        }
      }
      organizations.flush();
      providers.flush();
    }
  }

  /**
   * Export the payers.csv file. This method should be called once after all the
   * Patient records have been exported using the export(Person,long) method.
   *
   * @throws IOException if any IO errors occur.
   */
  public void exportPayers() throws IOException {
    // Export All Payers
    for (Payer payer : PayerManager.getAllPayers()) {
      payer(payer);
      payers.flush();
    }
    // Export No Insurance statistics
    payer(PayerManager.noInsurance);
    payers.flush();
  }

  /**
   * Export the payerTransitions.csv file. This method should be called once after all the
   * Patient records have been exported using the export(Person,long) method.
   *
   * @throws IOException if any IO errors occur.
   */
  private void exportPayerTransitions(Person person, long stopTime) throws IOException {
    for (PlanRecord plan : person.coverage.getPlanHistory()) {
      if (plan.start <= stopTime) {
        payerTransition(person, plan);
      }
    }
    payerTransitions.flush();
  }

  /**
   * Export the payerTransitions.csv file. This method should be called once after all the
   * Patient records have been exported using the export(Person,long) method.
   *
   * @throws IOException if any IO errors occur.
   */
  private void exportPatientExpenses(Person person, long stopTime) throws IOException {
    for (PlanRecord planRecord : person.coverage.getPlanHistory()) {
      if (planRecord.start <= stopTime) {
        patientExpense(person, planRecord);
      }
    }
    patientExpenses.flush();
  }

  /**
   * Add a single Person's health record info to the CSV records.
   *
   * @param person Person to write record data for
   * @param time   Time the simulation ended
   * @throws IOException if any IO error occurs
   */
  public void export(Person person, long time) throws IOException {

    String personID = patient(person, time);

    for (Encounter encounter : person.record.encounters) {

      String encounterID = encounter(person, personID, encounter);
      String payerID = encounter.claim.plan.getPayer().uuid;

      claim(person, encounter.claim, encounter, encounterID, time);

      for (HealthRecord.Entry condition : encounter.conditions) {
        /* condition to ignore codes other then retrieved from terminology url */
        if (!StringUtils.isEmpty(Config.get("generate.terminology_service_url"))
            && !RandomCodeGenerator.selectedCodes.isEmpty()) {
          if (RandomCodeGenerator.selectedCodes.stream()
              .filter(code -> code.code.equals(condition.codes.get(0).code))
              .findFirst().isPresent()) {
            condition(personID, encounterID, condition);
          }
        } else {
          condition(personID, encounterID, condition);
        }
      }

      for (HealthRecord.Allergy allergy : encounter.allergies) {
        allergy(personID, encounterID, allergy);
      }

      for (Observation observation : encounter.observations) {
        observation(personID, encounterID, observation);
      }

      for (Procedure procedure : encounter.procedures) {
        procedure(personID, encounterID, procedure);
      }

      for (Medication medication : encounter.medications) {
        medication(personID, encounterID, payerID, medication, time);
        claim(person, medication.claim, encounter, encounterID, time);
      }

      for (HealthRecord.Entry immunization : encounter.immunizations) {
        immunization(personID, encounterID, immunization);
      }

      for (CarePlan careplan : encounter.careplans) {
        careplan(person, personID, encounterID, careplan);
      }

      for (ImagingStudy imagingStudy : encounter.imagingStudies) {
        imagingStudy(person, personID, encounterID, imagingStudy);
      }

      for (Device device : encounter.devices) {
        device(personID, encounterID, device);
      }

      for (Supply supply : encounter.supplies) {
        supply(personID, encounterID, encounter, supply);
      }
    }
    CSVExporter.getInstance().exportPayerTransitions(person, time);
    CSVExporter.getInstance().exportPatientExpenses(person, time);
    int yearsOfHistory = Integer.parseInt(Config.get("exporter.years_of_history"));
    Calendar cutOff = new GregorianCalendar(1900, 0, 1);
    if (yearsOfHistory > 0) {
      cutOff = Calendar.getInstance();
      cutOff.set(cutOff.get(Calendar.YEAR) - yearsOfHistory, 0, 1);
    }
    Calendar now = Calendar.getInstance();
    Calendar birthDay = Calendar.getInstance();
    birthDay.setTimeInMillis((long) person.attributes.get(Person.BIRTHDATE));
    String[] gbdMetrics = { QualityOfLifeModule.QALY, QualityOfLifeModule.DALY,
        QualityOfLifeModule.QOLS };
    String unit = null;
    for (String score : gbdMetrics) {
      if (score.equals(QualityOfLifeModule.QOLS)) {
        unit = "{score}";
      } else {
        // years in UCUM is "a" for Latin "Annus"
        unit = "a";
      }
      @SuppressWarnings("unchecked")
      Map<Integer, Double> scores = (Map<Integer, Double>) person.attributes.get(score);
      for (Integer year : scores.keySet()) {
        birthDay.set(Calendar.YEAR, year);
        if (birthDay.after(cutOff) && birthDay.before(now)) {
          Observation obs = person.record.new Observation(
              birthDay.getTimeInMillis(), score, scores.get(year));
          obs.unit = unit;
          Code code = new Code("GBD", score, score);
          obs.codes.add(code);
          observation(personID, "", obs);
        }
      }
    }

    patients.flush();
    encounters.flush();
    conditions.flush();
    allergies.flush();
    medications.flush();
    careplans.flush();
    observations.flush();
    procedures.flush();
    immunizations.flush();
    imagingStudies.flush();
    devices.flush();
    supplies.flush();
    claims.flush();
    claimsTransactions.flush();
    patientExpenses.flush();
  }

  /**
   * Write a single Patient line, to patients.csv.
   *
   * @param person Person to write data for
   * @param time Time the simulation ended, to calculate age/deceased status
   * @return the patient's ID, to be referenced as a "foreign key" if necessary
   * @throws IOException if any IO error occurs
   */
  private String patient(Person person, long time) throws IOException {
    // Id,BIRTHDATE,DEATHDATE,SSN,DRIVERS,PASSPORT,PREFIX,
    // FIRST,LAST,SUFFIX,MAIDEN,MARITAL,RACE,ETHNICITY,GENDER,BIRTHPLACE,ADDRESS
    // CITY,STATE,COUNTY,FIPS,ZIP,LAT,LON,HEALTHCARE_EXPENSES,HEALTHCARE_COVERAGE,INCOME
    String personID = (String) person.attributes.get(Person.ID);

    // check if we've already exported this patient demographic data yet,
    // otherwise the "split record" feature could add a duplicate entry.
    if (person.attributes.containsKey("exported_to_csv")) {
      return personID;
    } else {
      person.attributes.put("exported_to_csv", personID);
    }

    StringBuilder s = new StringBuilder();
    s.append(personID).append(',');
    s.append(dateFromTimestamp((long) person.attributes.get(Person.BIRTHDATE))).append(',');
    if (!person.alive(time)) {
      s.append(dateFromTimestamp((Long) person.attributes.get(Person.DEATHDATE)));
    }

    for (String attribute : new String[] {
        Person.IDENTIFIER_SSN,
        Person.IDENTIFIER_DRIVERS,
        Person.IDENTIFIER_PASSPORT,
        Person.NAME_PREFIX,
        Person.FIRST_NAME,
        Person.LAST_NAME,
        Person.NAME_SUFFIX,
        Person.MAIDEN_NAME,
        Person.MARITAL_STATUS,
        Person.RACE,
        Person.ETHNICITY,
        Person.GENDER,
        Person.BIRTHPLACE,
        Person.ADDRESS,
        Person.CITY,
        Person.STATE,
        Person.COUNTY,
        Person.FIPS,
        Person.ZIP,
    }) {
      String value = (String) person.attributes.getOrDefault(attribute, "");
      s.append(',').append(clean(value));
    }
    // LAT,LON
    s.append(',').append(person.getY()).append(',').append(person.getX()).append(',');
    // HEALTHCARE_EXPENSES
    s.append(person.coverage.getTotalHealthcareExpenses()).append(',');
    // HEALTHCARE_COVERAGE
    s.append(person.coverage.getTotalCoverage()).append(',');
    // INCOME
    s.append(person.attributes.get(Person.INCOME));
    // QALYS
    // s.append(person.attributes.get("most-recent-qaly")).append(',');
    // DALYS
    // s.append(person.attributes.get("most-recent-daly"));

    s.append(NEWLINE);
    write(s.toString(), patients);

    return personID;
  }

  /**
   * Write a single Encounter line to encounters.csv.
   *
   * @param rand      Source of randomness to use when generating ids etc
   * @param personID  The ID of the person that had this encounter
   * @param encounter The encounter itself
   * @return The encounter ID, to be referenced as a "foreign key" if necessary
   * @throws IOException if any IO error occurs
   */
  private String encounter(RandomNumberGenerator rand, String personID,
          Encounter encounter) throws IOException {
    // Id,START,STOP,PATIENT,ORGANIZATION,PROVIDER,PAYER,ENCOUNTERCLASS,CODE,DESCRIPTION,
    // BASE_ENCOUNTER_COST,TOTAL_CLAIM_COST,PAYER_COVERAGE,REASONCODE,REASONDESCRIPTION
    StringBuilder s = new StringBuilder();

    String encounterID = rand.randUUID().toString();
    // ID
    s.append(encounterID).append(',');
    // START
    s.append(iso8601Timestamp(encounter.start)).append(',');
    // STOP
    if (encounter.stop != 0L) {
      s.append(iso8601Timestamp(encounter.stop)).append(',');
    } else {
      s.append(',');
    }
    // PATIENT
    s.append(personID).append(',');
    // ORGANIZATION
    if (encounter.provider != null) {
      s.append(encounter.provider.getResourceID()).append(',');
    } else {
      s.append(',');
    }
    // PROVIDER
    if (encounter.clinician != null) {
      s.append(encounter.clinician.getResourceID()).append(',');
    } else {
      s.append(',');
    }
    // PAYER
    if (encounter.claim.plan.getPayer() != null) {
      s.append(encounter.claim.plan.getPayer().getResourceID()).append(',');
    } else {
      s.append(',');
    }
    // ENCOUNTERCLASS
    if (encounter.type != null) {
      s.append(encounter.type.toLowerCase()).append(',');
    } else {
      s.append(',');
    }
    // CODE
    Code coding = null;

    coding = encounter.codes.get(0);
    // CODE
    s.append(coding.code).append(',');
    // DESCRIPTION
    s.append(clean(coding.display)).append(',');

    // BASE_ENCOUNTER_COST
    s.append(String.format(Locale.US, "%.2f", encounter.getCost())).append(',');
    // TOTAL_COST
    s.append(String.format(Locale.US, "%.2f", encounter.claim.getTotalClaimCost())).append(',');
    // PAYER_COVERAGE
    s.append(String.format(Locale.US, "%.2f", encounter.claim.getTotalCoveredCost())).append(',');
    // REASONCODE & REASONDESCRIPTION
    if (encounter.reason == null) {
      s.append(",");
    } else {
      s.append(encounter.reason.code).append(',');
      s.append(clean(encounter.reason.display));
    }

    s.append(NEWLINE);
    write(s.toString(), encounters);

    return encounterID;
  }

  /**
   * Write a single Condition to conditions.csv.
   *
   * @param personID    ID of the person that has the condition.
   * @param encounterID ID of the encounter where the condition was diagnosed
   * @param condition   The condition itself
   * @throws IOException if any IO error occurs
   */
  private void condition(String personID, String encounterID, Entry condition) throws IOException {
    // START,STOP,PATIENT,ENCOUNTER,CODE,DESCRIPTION
    StringBuilder s = new StringBuilder();

    s.append(dateFromTimestamp(condition.start)).append(',');
    if (condition.stop != 0L) {
      s.append(dateFromTimestamp(condition.stop));
    }
    s.append(',');
    s.append(personID).append(',');
    s.append(encounterID).append(',');

    Code coding = condition.codes.get(0);

    s.append(coding.code).append(',');
    s.append(clean(coding.display));

    s.append(NEWLINE);
    write(s.toString(), conditions);
  }

  /**
   * Write a single Allergy to allergies.csv.
   *
   * @param personID    ID of the person that has the allergy.
   * @param encounterID ID of the encounter where the allergy was diagnosed
   * @param allergy     The allergy itself
   * @throws IOException if any IO error occurs
   */
  private void allergy(String personID, String encounterID, HealthRecord.Allergy allergy)
      throws IOException {
    // START,STOP,PATIENT,ENCOUNTER,CODE,SYSTEM,DESCRIPTION,TYPE,CATEGORY
    // REACTION1,DESCRIPTION1,SEVERITY1,
    // REACTION2,DESCRIPTION2,SEVERITY2
    StringBuilder s = new StringBuilder();

    s.append(dateFromTimestamp(allergy.start)).append(',');
    if (allergy.stop != 0L) {
      s.append(dateFromTimestamp(allergy.stop));
    }
    s.append(',');
    s.append(personID).append(',');
    s.append(encounterID).append(',');

    Code coding = allergy.codes.get(0);

    s.append(coding.code).append(',');
    s.append(getSystemFromURI(coding.system)).append(',');
    s.append(clean(coding.display)).append(',');
    if (allergy.allergyType != null) {
      s.append(allergy.allergyType);
    }
    s.append(',');
    if (allergy.category != null) {
      s.append(allergy.category);
    }
    s.append(',');

    int reactionsSize = 0;
    if (allergy.reactions != null) {
      reactionsSize = allergy.reactions.size();
    }
    Function<Map.Entry<HealthRecord.Code, HealthRecord.ReactionSeverity>, String> template =
        mapEntry -> {
          StringBuilder reactionBuilder = new StringBuilder();
          reactionBuilder.append(mapEntry.getKey().code).append(',');
          reactionBuilder.append(clean(mapEntry.getKey().display)).append(',');
          reactionBuilder.append(mapEntry.getValue());
          return reactionBuilder.toString();
        };

    switch (reactionsSize) {
      case 0:
        s.append(",,,,,");
        break;
      case 1:
        s.append(allergy.reactions.entrySet().stream().map(template).collect(Collectors.joining()));
        s.append(",,,");
        break;
      default:
        //case where there are more than two reactions so we need to support by severity
        s.append(allergy.reactions.entrySet().stream()
            .sorted(Comparator.comparing(Map.Entry::getValue))
            .limit(2)
            .map(template)
            .collect(Collectors.joining(",")));
    }

    s.append(NEWLINE);
    write(s.toString(), allergies);
  }

  /**
   * Write a single Observation to observations.csv.
   *
   * @param personID    ID of the person to whom the observation applies.
   * @param encounterID ID of the encounter where the observation was taken
   * @param observation The observation itself
   * @throws IOException if any IO error occurs
   */
  private void observation(String personID,
      String encounterID, Observation observation) throws IOException {

    if (observation.value == null) {
      if (observation.observations != null && !observation.observations.isEmpty()) {
        // just loop through the child observations

        for (Observation subObs : observation.observations) {
          observation(personID, encounterID, subObs);
        }
      }

      // no value so nothing more to report here
      return;
    }

    // DATE,PATIENT,ENCOUNTER,CATEGORY,CODE,DESCRIPTION,VALUE,UNITS
    StringBuilder s = new StringBuilder();

    s.append(iso8601Timestamp(observation.start)).append(',');
    s.append(personID).append(',');
    s.append(encounterID).append(',');
    if (observation.category != null) {
      s.append(observation.category);
    }
    s.append(',');

    Code coding = observation.codes.get(0);

    s.append(coding.code).append(',');
    s.append(clean(coding.display)).append(',');

    String value = ExportHelper.getObservationValue(observation);
    String type = ExportHelper.getObservationType(observation);
    s.append(clean(value)).append(',');
    s.append(clean(observation.unit)).append(',');
    s.append(type);

    s.append(NEWLINE);
    write(s.toString(), observations);
  }

  /**
   * Write a single Procedure to procedures.csv.
   *
   * @param personID    ID of the person on whom the procedure was performed.
   * @param encounterID ID of the encounter where the procedure was performed
   * @param procedure   The procedure itself
   * @throws IOException if any IO error occurs
   */
  private void procedure(String personID, String encounterID,
      Procedure procedure) throws IOException {
    // START,STOP,PATIENT,ENCOUNTER,CODE,DESCRIPTION,COST,REASONCODE,REASONDESCRIPTION
    StringBuilder s = new StringBuilder();

    s.append(iso8601Timestamp(procedure.start)).append(',');
    if (procedure.stop != 0L) {
      s.append(iso8601Timestamp(procedure.stop));
    }
    s.append(',');
    s.append(personID).append(',');
    s.append(encounterID).append(',');
    // CODE
    Code coding = procedure.codes.get(0);
    s.append(coding.code).append(',');
    // DESCRIPTION
    s.append(clean(coding.display)).append(',');
    // BASE_COST
    s.append(String.format(Locale.US, "%.2f", procedure.getCost())).append(',');
    // REASONCODE & REASONDESCRIPTION
    if (procedure.reasons.isEmpty()) {
      s.append(','); // reason code & desc
    } else {
      Code reason = procedure.reasons.get(0);
      s.append(reason.code).append(',');
      s.append(clean(reason.display));
    }

    s.append(NEWLINE);
    write(s.toString(), procedures);
  }

  /**
   * Write a single Medication to medications.csv.
   *
   * @param personID    ID of the person prescribed the medication.
   * @param encounterID ID of the encounter where the medication was prescribed
   * @param payerID     ID of the payer who covered the immunization.
   * @param medication  The medication itself
   * @param stopTime    End time
   * @throws IOException if any IO error occurs
   */
  private void medication(String personID, String encounterID, String payerID,
      Medication medication, long stopTime)
      throws IOException {
    // START,STOP,PATIENT,PAYER,ENCOUNTER,CODE,DESCRIPTION,
    // BASE_COST,PAYER_COVERAGE,DISPENSES,TOTALCOST,REASONCODE,REASONDESCRIPTION
    StringBuilder s = new StringBuilder();

    s.append(iso8601Timestamp(medication.start)).append(',');
    if (medication.stop != 0L) {
      s.append(iso8601Timestamp(medication.stop));
    }
    s.append(',');
    s.append(personID).append(',');
    s.append(payerID).append(',');
    s.append(encounterID).append(',');
    // CODE
    Code coding = medication.codes.get(0);
    s.append(coding.code).append(',');
    // DESCRIPTION
    s.append(clean(coding.display)).append(',');
    // BASE_COST
    BigDecimal cost = medication.getCost();
    s.append(String.format(Locale.US, "%.2f", cost)).append(',');
    // PAYER_COVERAGE
    s.append(String.format(Locale.US, "%.2f", medication.claim.getTotalCoveredCost())).append(',');
    long dispenses = 1; // dispenses = refills + original
    // makes the math cleaner and more explicit. dispenses * unit cost = total cost

    long stop = medication.stop;
    if (stop == 0L) {
      stop = stopTime;
    }
    long medDuration = stop - medication.start;

    if (medication.prescriptionDetails != null && medication.prescriptionDetails.has("refills")) {
      dispenses = medication.prescriptionDetails.get("refills").getAsInt();
    } else if (medication.prescriptionDetails != null
        && medication.prescriptionDetails.has("duration")) {
      JsonObject duration = medication.prescriptionDetails.getAsJsonObject("duration");

      long quantity = duration.get("quantity").getAsLong();
      String unit = duration.get("unit").getAsString();
      long durationMs = Utilities.convertTime(unit, quantity);
      dispenses = medDuration / durationMs;
    } else {
      // assume 1 refill / month
      long durationMs = Utilities.convertTime("months", 1);
      dispenses = medDuration / durationMs;
    }

    if (dispenses < 1) {
      // integer division could leave us with 0,
      // ex. if the active time (start->stop) is less than the provided duration
      // or less than a month if no duration provided
      dispenses = 1;
    }

    s.append(dispenses).append(',');
    BigDecimal totalCost = cost.multiply(
        BigDecimal.valueOf(dispenses)).setScale(2, RoundingMode.DOWN); //Truncate 2 decimal places
    s.append(String.format(Locale.US, "%.2f", totalCost)).append(',');

    if (medication.reasons.isEmpty()) {
      s.append(','); // reason code & desc
    } else {
      Code reason = medication.reasons.get(0);
      s.append(reason.code).append(',');
      s.append(clean(reason.display));
    }

    s.append(NEWLINE);
    write(s.toString(), medications);
  }

  /**
   * Write a single Immunization to immunizations.csv.
   *
   * @param personID     ID of the person on whom the immunization was performed.
   * @param encounterID  ID of the encounter where the immunization was performed.
   * @param immunization The immunization itself
   * @throws IOException if any IO error occurs
   */
  private void immunization(String personID, String encounterID,
      Entry immunization) throws IOException {
    // DATE,PATIENT,ENCOUNTER,CODE,DESCRIPTION,BASE_COST
    StringBuilder s = new StringBuilder();

    s.append(iso8601Timestamp(immunization.start)).append(',');
    s.append(personID).append(',');
    s.append(encounterID).append(',');
    // CODE
    Code coding = immunization.codes.get(0);
    s.append(coding.code).append(',');
    // DESCRIPTION
    s.append(clean(coding.display)).append(',');
    // BASE_COST
    s.append(String.format(Locale.US, "%.2f", immunization.getCost()));

    s.append(NEWLINE);
    write(s.toString(), immunizations);
  }

  /**
   * Write a single CarePlan to careplans.csv.
   *
   * @param rand        Source of randomness to use when generating ids etc
   * @param personID    ID of the person prescribed the careplan.
   * @param encounterID ID of the encounter where the careplan was prescribed
   * @param careplan    The careplan itself
   * @throws IOException if any IO error occurs
   */
  private String careplan(RandomNumberGenerator rand, String personID, String encounterID,
      CarePlan careplan) throws IOException {
    // Id,START,STOP,PATIENT,ENCOUNTER,CODE,DESCRIPTION,REASONCODE,REASONDESCRIPTION
    StringBuilder s = new StringBuilder();

    String careplanID = rand.randUUID().toString();
    s.append(careplanID).append(',');
    s.append(dateFromTimestamp(careplan.start)).append(',');
    if (careplan.stop != 0L) {
      s.append(dateFromTimestamp(careplan.stop));
    }
    s.append(',');
    s.append(personID).append(',');
    s.append(encounterID).append(',');

    Code coding = careplan.codes.get(0);

    s.append(coding.code).append(',');
    s.append(coding.display).append(',');

    if (careplan.reasons.isEmpty()) {
      s.append(','); // reason code & desc
    } else {
      Code reason = careplan.reasons.get(0);
      s.append(reason.code).append(',');
      s.append(clean(reason.display));
    }
    s.append(NEWLINE);

    write(s.toString(), careplans);

    return careplanID;
  }

  /**
   * Write a single ImagingStudy to imaging_studies.csv.
   *
   * @param rand         Source of randomness to use when generating ids etc
   * @param personID     ID of the person the ImagingStudy was taken of.
   * @param encounterID  ID of the encounter where the ImagingStudy was performed
   * @param imagingStudy The ImagingStudy itself
   * @throws IOException if any IO error occurs
   */
  private String imagingStudy(RandomNumberGenerator rand, String personID, String encounterID,
      ImagingStudy imagingStudy) throws IOException {
    // Id,DATE,PATIENT,ENCOUNTER,SERIES_UID,BODYSITE_CODE,BODYSITE_DESCRIPTION,
    // MODALITY_CODE,MODALITY_DESCRIPTION,INSTANCE_UID,SOP_CODE,SOP_DESCRIPTION,PROCEDURE_CODE
    StringBuilder s = new StringBuilder();

    String studyID = rand.randUUID().toString();

    for (ImagingStudy.Series series: imagingStudy.series) {
      String seriesDicomUid = series.dicomUid;
      Code bodySite = series.bodySite;
      Code modality = series.modality;
      for (ImagingStudy.Instance instance: series.instances) {
        String instanceDicomUid = instance.dicomUid;
        Code sopClass = instance.sopClass;
        s.append(studyID).append(',');
        s.append(iso8601Timestamp(imagingStudy.start)).append(',');
        s.append(personID).append(',');
        s.append(encounterID).append(',');

        s.append(seriesDicomUid).append(',');

        s.append(bodySite.code).append(',');
        s.append(bodySite.display).append(',');

        s.append(modality.code).append(',');
        s.append(modality.display).append(',');

        s.append(instanceDicomUid).append(',');

        s.append(sopClass.code).append(',');
        s.append(sopClass.display).append(',');
        s.append(imagingStudy.codes.get(0).code);

        s.append(NEWLINE);
      }
    }

    write(s.toString(), imagingStudies);

    return studyID;
  }

  /**
   * Write a single Device to devices.csv.
   *
   * @param personID     ID of the person the Device is affixed to.
   * @param encounterID  ID of the encounter where the Device was associated
   * @param device       The Device itself
   * @throws IOException if any IO error occurs
   */
  private void device(String personID, String encounterID, Device device)
      throws IOException {
    // START,STOP,PATIENT,ENCOUNTER,CODE,DESCRIPTION,UDI
    StringBuilder s = new StringBuilder();

    s.append(iso8601Timestamp(device.start)).append(',');
    if (device.stop != 0L) {
      s.append(iso8601Timestamp(device.stop));
    }
    s.append(',');

    s.append(personID).append(',');
    s.append(encounterID).append(',');

    Code code = device.codes.get(0);
    s.append(code.code).append(',');
    s.append(clean(code.display)).append(',');

    s.append(device.udi);

    s.append(NEWLINE);

    write(s.toString(), devices);
  }

  /**
   * Write a single Supply to supplies.csv.
   *
   * @param personID     ID of the person the supply was used for.
   * @param encounterID  ID of the encounter where the supply was used
   * @param supply       The supply itself
   * @throws IOException if any IO error occurs
   */
  private void supply(String personID, String encounterID, Encounter encounter, Supply supply)
          throws IOException {
    // DATE,PATIENT,ENCOUNTER,CODE,DESCRIPTION,QUANTITY
    StringBuilder s = new StringBuilder();

    s.append(dateFromTimestamp(supply.start)).append(',');
    s.append(personID).append(',');
    s.append(encounterID).append(',');

    Code code = supply.codes.get(0);
    s.append(code.code).append(',');
    s.append(clean(code.display)).append(',');

    s.append(supply.quantity);

    s.append(NEWLINE);

    write(s.toString(), supplies);
  }

  /**
   * Write a single organization to organizations.csv
   *
   * @param org         The organization to be written
   * @param utilization The total number of encounters for the org
   * @throws IOException if any IO error occurs
   */
  private void organization(Provider org, int utilization) throws IOException {
    // Id,NAME,ADDRESS,CITY,STATE,ZIP,PHONE,REVENUE,UTILIZATION
    StringBuilder s = new StringBuilder();
    s.append(org.getResourceID()).append(',');
    s.append(clean(org.name)).append(',');
    s.append(clean(org.address)).append(',');
    s.append(org.city).append(',');
    s.append(org.state).append(',');
    s.append(org.zip).append(',');
    s.append(org.getY()).append(',');
    s.append(org.getX()).append(',');
    s.append(org.phone).append(',');
    s.append(org.getRevenue()).append(',');
    s.append(utilization);
    s.append(NEWLINE);

    write(s.toString(), organizations);
  }

  /**
   * Write a single clinician to providers.csv
   *
   * @param provider The provider information to be written
   * @param orgId    ID of the organization the provider belongs to
   * @throws IOException if any IO error occurs
   */
  private void provider(Clinician provider, String orgId) throws IOException {
    // Id,ORGANIZATION,NAME,GENDER,SPECIALITY,ADDRESS,CITY,STATE,ZIP,ENCOUNTERS,PROCEDURES

    StringBuilder s = new StringBuilder();
    s.append(provider.getResourceID()).append(',');
    s.append(orgId).append(',');
    for (String attribute : new String[] { Clinician.NAME, Clinician.GENDER,
        Clinician.SPECIALTY, Clinician.ADDRESS, Clinician.CITY, Clinician.STATE,
        Clinician.ZIP }) {
      String value = (String) provider.attributes.getOrDefault(attribute, "");
      s.append(clean(value)).append(',');
    }
    s.append(provider.getY()).append(',');
    s.append(provider.getX()).append(',');
    s.append(provider.getEncounterCount()).append(',');
    s.append(provider.getProcedureCount());

    s.append(NEWLINE);

    write(s.toString(), providers);
  }

  /**
   * Write a single payer to payers.csv.
   *
   * @param payer The payer to be exported.
   * @throws IOException if any IO error occurs.
   */
  private void payer(Payer payer) throws IOException {
    // Id,NAME,OWNERSHIP,ADDRESS,CITY,STATE_HEADQUARTERED,ZIP,PHONE,AMOUNT_COVERED,
    // AMOUNT_UNCOVERED,REVENUE,
    // COVERED_ENCOUNTERS,UNCOVERED_ENCOUNTERS,COVERED_MEDICATIONS,UNCOVERED_MEDICATIONS,
    // COVERED_PROCEDURES,UNCOVERED_PROCEDURES,COVERED_IMMUNIZATIONS,UNCOVERED_IMMUNIZATIONS,
    // UNIQUE_CUSTOMERS,QOLS_AVG,MEMBER_MONTHS

    StringBuilder s = new StringBuilder();
    // UUID
    s.append(payer.getResourceID()).append(',');
    // NAME
    s.append(payer.getName()).append(',');
    // OWNERSHIP
    s.append(payer.getOwnership()).append(',');
    // Second Class Attributes
    for (String attribute : new String[]
        { "address", "city", "state_headquartered", "zip", "phone" }) {
      String value = (String) payer.getAttributes().getOrDefault(attribute, "");
      s.append(clean(value)).append(',');
    }
    // AMOUNT_COVERED
    s.append(String.format(Locale.US, "%.2f", payer.getAmountCovered())).append(',');
    // AMOUNT_UNCOVERED
    s.append(String.format(Locale.US, "%.2f", payer.getAmountUncovered())).append(',');
    // REVENUE
    s.append(String.format(Locale.US, "%.2f", payer.getRevenue())).append(',');
    // Covered/Uncovered Encounters/Medications/Procedures/Immunizations
    s.append(payer.getEncountersCoveredCount()).append(",");
    s.append(payer.getEncountersUncoveredCount()).append(",");
    s.append(payer.getMedicationsCoveredCount()).append(",");
    s.append(payer.getMedicationsUncoveredCount()).append(",");
    s.append(payer.getProceduresCoveredCount()).append(",");
    s.append(payer.getProceduresUncoveredCount()).append(",");
    s.append(payer.getImmunizationsCoveredCount()).append(",");
    s.append(payer.getImmunizationsUncoveredCount()).append(",");
    // UNIQUE_CUSTOMERS
    s.append(payer.getUniqueCustomers()).append(",");
    // QOLS_AVG
    s.append(payer.getQolsAverage()).append(",");
    // MEMBER_MONTHS (Note that this converts the number of years covered to months)
    s.append(payer.getNumYearsCovered() * 12);

    s.append(NEWLINE);
    write(s.toString(), payers);
  }

  /**
   * Write a single range of unchanged payer history to payer_transitions.csv
   *
   * @param person The person whose payer history to write.
   * @param planRecord The plan
   * @throws IOException if any IO error occurs
   */
  private void payerTransition(Person person, PlanRecord planRecord) throws IOException {
    // PATIENT_ID,MEMBER_ID,START_YEAR,END_YEAR,PAYER_ID,SECONDARY_PAYER_ID,OWNERSHIP,OWNERNAME

    StringBuilder s = new StringBuilder();
    // PATIENT_ID
    s.append(person.attributes.get(Person.ID)).append(",");
    // MEMBER_ID
    if (planRecord.id != null) {
      s.append(planRecord.id);
    }
    s.append(",");
    // START_YEAR
    s.append(iso8601Timestamp(planRecord.start)).append(',');
    // END_YEAR
    s.append(iso8601Timestamp(planRecord.stop)).append(',');
    // PAYER_ID
    s.append(planRecord.plan.getPayer().getResourceID()).append(',');
    // SECONDARY_PAYER_ID
    if (planRecord.secondaryPlan.getPayer() != null && !planRecord.secondaryPlan.getPayer()
        .isNoInsurance()) {
      s.append(planRecord.secondaryPlan.getPayer().getResourceID());
    }
    s.append(',');
    // PLAN_OWNERSHIP
    if (planRecord.owner != null) {
      s.append(planRecord.owner);
    }
    s.append(',');
    // OWNERNAME
    if (planRecord.ownerName != null) {
      s.append(planRecord.ownerName);
    }
    s.append(NEWLINE);
    write(s.toString(), payerTransitions);
  }

  private void patientExpense(Person person, PlanRecord planRecord) throws IOException {
    // PATIENT_ID,YEAR,PAYER_ID,HEALTHCARE_EXPENSES,INSURANCE_COSTS,COVERED_COSTS

    StringBuilder s = new StringBuilder();
    // PATIENT_ID
    s.append(person.attributes.get(Person.ID)).append(",");
    // YEAR
    s.append(iso8601Timestamp(planRecord.start)).append(',');
    // PAYER_ID
    s.append(planRecord.plan.getPayer().getResourceID()).append(',');
    // HEALTHCARE_EXPENSES
    double healthcareExpenses = planRecord.getHealthcareExpenses().doubleValue();
    s.append(healthcareExpenses).append(',');
    // INSURANCE_COSTS
    double insuranceCosts = planRecord.getInsuranceCosts().doubleValue();
    s.append(insuranceCosts).append(',');
    // COVERED_COSTS
    double coveredExpenses = planRecord.getCoveredExpenses().doubleValue();
    s.append(coveredExpenses);

    s.append(NEWLINE);
    write(s.toString(), patientExpenses);
  }

  /**
   * Return a department code for the claim.
   * @param encounter The encounter being billed.
   * @param patient The patient.
   * @return The department code.
   */
  private String claimDepartmentCode(Encounter encounter, Person patient) {
    String dept = "99";
    if (encounter.type != null) {
      EncounterType type = EncounterType.fromString(encounter.type);
      if (type != null) {
        dept = "" + type.ordinal();
        if (type == EncounterType.WELLNESS) {
          if (patient.ageInYears(encounter.start) < 18) {
            dept = "10"; // pediatric
          } else {
            dept = "20"; // adult primary
          }
        }
      }
    }
    return dept;
  }

  /**
   * Write a single claim to claims.csv.
   *
   * @param claim The claim to be exported.
   * @throws IOException if any IO error occurs.
   */
  private void claim(RandomNumberGenerator rand, Claim claim, Encounter encounter,
      String encounterID, long time) throws IOException {
    // Id,PATIENTID,PROVIDERID,PRIMARYPATIENTINSURANCEID,SECONDARYPATIENTINSURANCEID,
    // DEPARTMENTID,PATIENTDEPARTMENTID,DIAGNOSIS1,DIAGNOSIS2,DIAGNOSIS3,DIAGNOSIS4,
    // DIAGNOSIS5,DIAGNOSIS6,DIAGNOSIS7,DIAGNOSIS8,REFERRINGPROVIDERID,APPOINTMENTID,
    // CURRENTILLNESSDATE,SERVICEDATE,SUPERVISINGPROVIDERID,STATUS1,STATUS2,STATUSP,
    // OUTSTANDING1,OUTSTANDING2,OUTSTANDINGP,LASTBILLEDDATE1,LASTBILLEDDATE2,LASTBILLEDDATEP,
    // HEALTHCARECLAIMTYPEID1,HEALTHCARECLAIMTYPEID2

    // Institutional or Professional?
    boolean institutional = true;
    if (encounter.provider != null) {
      institutional = encounter.provider.institutional;
    }

    StringBuilder s = new StringBuilder();
    // Claim Id. Should be a number.
    String claimId = rand.randUUID().toString();
    s.append(claimId).append(',');
    // PATIENTID
    s.append(claim.person.attributes.get(Person.ID)).append(',');
    // Provider, should not be null.
    if (encounter.clinician != null) {
      s.append(encounter.clinician.getResourceID()).append(',');
    } else {
      s.append(',');
    }
    // PRIMARYPATIENTINSURANCEID
    if (encounter.claim.plan.getPayer() == null
        || encounter.claim.plan.isNoInsurance()) {
      s.append("0,"); // 0 == No Insurance
    } else {
      s.append(claim.plan.getPayer().getResourceID()).append(',');
    }
    // SECONDARYPATIENTINSURANCEID (0 default if none)
    if (encounter.claim.secondaryPlan == null
        || encounter.claim.secondaryPlan.isNoInsurance()) {
      s.append("0,");
    } else {
      s.append(claim.secondaryPlan.getPayer().getResourceID()).append(',');
    }
    // DEPARTMENTID
    String departmentId = claimDepartmentCode(encounter, claim.person);
    s.append(departmentId).append(',');
    s.append(departmentId).append(',');
    // Diagnosis codes and illness onset
    int dxCode = 0;
    Long[] onset = new Long[8];
    String[] diagnosisCodes = new String[8];
    if (encounter.reason != null) {
      diagnosisCodes[dxCode] = encounter.reason.code;
      onset[dxCode] = claim.person.record.presentOnset(diagnosisCodes[dxCode]);
      dxCode++;
    }
    Iterator<HealthRecord.Entry> items = encounter.conditions.iterator();
    while ((dxCode < diagnosisCodes.length) && items.hasNext()) {
      Entry item = items.next();
      diagnosisCodes[dxCode] = item.codes.get(0).code;
      onset[dxCode] = claim.person.record.presentOnset(diagnosisCodes[dxCode]);
      dxCode++;
    }
    if (dxCode == 0) {
      // There must be a diagnosis code, if there aren't any (e.g. wellness visit
      // where nothing is wrong) then add the encounter code.
      diagnosisCodes[dxCode] = encounter.codes.get(0).code;
      onset[dxCode] = claim.person.record.presentOnset(diagnosisCodes[dxCode]);
    }
    for (String diagnosisCode : diagnosisCodes) {
      if (diagnosisCode != null && !diagnosisCode.isEmpty()) {
        s.append(diagnosisCode).append(',');
      } else {
        s.append(',');
      }
    }
    Long onsetIllness = encounter.start;
    for (Long onsetTime : onset) {
      if (onsetTime != null && onsetTime < onsetIllness) {
        onsetIllness = onsetTime;
      }
    }
    // TODO REFERRINGPROVIDERID
    s.append(',');
    // APPOINTMENTID
    s.append(encounterID).append(',');
    // CURRENTILLNESSDATE
    s.append(iso8601Timestamp(onsetIllness)).append(',');
    // SERVICEDATE
    s.append(iso8601Timestamp(encounter.start)).append(',');
    // SUPERVISINGPROVIDERID
    if (encounter.clinician != null) {
      s.append(encounter.clinician.getResourceID()).append(',');
    } else {
      s.append(',');
    }
    if (time > encounter.stop) {
      // STATUS1 for Payer1
      s.append("CLOSED,");
      // STATUS2 for Payer2
      if (claim.secondaryPlan != null
          && claim.secondaryPlan.isNoInsurance()) {
        s.append("CLOSED,");
      } else {
        s.append(',');
      }
      // STATUSP for Patient as Payer
      s.append("CLOSED,");
      // OUTSTANDING1
      s.append("0,");
      // OUTSTANDING2
      if (claim.secondaryPlan != null
          && claim.secondaryPlan.isNoInsurance()) {
        s.append("0,");
      } else {
        s.append(',');
      }
      // OUTSTANDINGP
      s.append("0,");
      // LASTBILLEDDATE1
      s.append(iso8601Timestamp(encounter.stop)).append(',');
      // LASTBILLEDDATE2
      if (claim.secondaryPlan != null
          && claim.secondaryPlan.isNoInsurance()) {
        s.append(iso8601Timestamp(encounter.stop)).append(',');
      } else {
        s.append(',');
      }
      // LASTBILLEDDATEP
      s.append(iso8601Timestamp(encounter.stop)).append(',');

    } else {
      // TODO this may not get triggered
      // STATUS1 for Payer1
      s.append("BILLED,");
      // STATUS2 for Payer2
      if (claim.secondaryPlan != null
          && claim.secondaryPlan.isNoInsurance()) {
        s.append("BILLED,");
      } else {
        s.append(',');
      }
      // STATUSP for Patient as Payer
      s.append("BILLED,");
      // OUTSTANDING1 (TODO this should be the outstanding payer balance)
      s.append(String.format(Locale.US, "%.2f", encounter.claim.getTotalCoveredCost()))
              .append(',');
      // OUTSTANDING2
      if (claim.secondaryPlan != null
          && claim.secondaryPlan.isNoInsurance()) {
        // TODO this is not correct
        s.append(String.format(Locale.US, "%.2f", encounter.claim.getTotalCoveredCost()))
                .append(',');
      } else {
        s.append(',');
      }
      // OUTSTANDINGP (TODO this should be the outstanding patient balance)
      BigDecimal patientCost = claim.getTotalClaimCost().subtract(claim.getTotalCoveredCost());
      s.append(String.format(Locale.US, "%.2f", patientCost)).append(',');
      // LASTBILLEDDATE1
      s.append(iso8601Timestamp(encounter.start)).append(',');
      // LASTBILLEDDATE2
      if (claim.secondaryPlan != null
          && claim.secondaryPlan.isNoInsurance()) {
        s.append(iso8601Timestamp(encounter.start)).append(',');
      } else {
        s.append(',');
      }
      // LASTBILLEDDATEP
      s.append(iso8601Timestamp(encounter.start)).append(',');
    }

    // HEALTHCARECLAIMTYPEID1
    if (institutional) {
      s.append("2,");
    } else {
      s.append("1,");
    }
    // HEALTHCARECLAIMTYPEID2
    if (claim.secondaryPlan != null
        && claim.secondaryPlan.isNoInsurance()) {
      if (institutional) {
        s.append('2');
      } else {
        s.append('1');
      }
    } else {
      s.append('0');
    }
    s.append(NEWLINE);
    write(s.toString(), claims);

    // Main Claim
    simulateClaimProcess(rand, claim, claimId, encounter, encounterID, claim.mainEntry,
        diagnosisCodes, departmentId, true);

    // Each Entry...
    for (int i = 0; i < claim.items.size(); i++) {
      Claim.ClaimEntry claimEntry = claim.items.get(i);
      Entry entry = claimEntry.entry;
      if ((entry instanceof HealthRecord.Procedure)
          || (entry instanceof HealthRecord.Immunization)
          || (entry instanceof HealthRecord.Medication)) {
        simulateClaimProcess(rand, claim, claimId, encounter, encounterID, claimEntry,
            diagnosisCodes, departmentId, false);
      }
    }
  }

  private void simulateClaimProcess(RandomNumberGenerator rand, Claim claim, String claimId,
      Encounter encounter, String encounterId, Claim.ClaimEntry claimEntry,
      String[] diagnosisCodes, String departmentId, boolean mainEntry) throws IOException {
    long chargeId = transactionId.getAndIncrement();
    // CHARGE
    ClaimTransaction t = new ClaimTransaction(encounter, encounterId,
        claim, claimId, chargeId, claimEntry, rand);
    t.type = ClaimTransactionType.CHARGE;
    t.setAmount(claimEntry.cost);
    t.departmentId = departmentId;
    t.diagnosisCodes = diagnosisCodes;
    if (claim.plan.isNoInsurance()) {
      t.transferType = "p";
    } else {
      t.transferType = "1";
    }
    write(t.toString(), claimsTransactions);
    chargeId = transactionId.getAndIncrement();

    BigDecimal remainder = claimEntry.cost;
    if (mainEntry) {
      if (claimEntry.copayPaidByPatient.compareTo(Claim.ZERO_CENTS) > 0) {
        // COPAY
        remainder = remainder.subtract(claimEntry.copayPaidByPatient);
        if (remainder.compareTo(Claim.ZERO_CENTS) < 0) {
          // If the cost of the copay is greater than the medication cost.
          remainder = Claim.ZERO_CENTS;
        }
        t = new ClaimTransaction(encounter, encounterId,
            claim, claimId, chargeId, claimEntry, rand);
        t.type = ClaimTransactionType.PAYMENT;
        t.method = PaymentMethod.COPAY;
        t.payment = claimEntry.copayPaidByPatient;
        t.unpaid = remainder;
        t.departmentId = departmentId;
        t.diagnosisCodes = diagnosisCodes;
        write(t.toString(), claimsTransactions);
        chargeId = transactionId.getAndIncrement();
      }
    }

    // ADJUSTMENTS
    if (claimEntry.adjustment.compareTo(Claim.ZERO_CENTS) > 0) {
      remainder = remainder.subtract(claimEntry.adjustment);
      if (remainder.compareTo(Claim.ZERO_CENTS) < 0) {
        remainder = Claim.ZERO_CENTS;
      }
      t = new ClaimTransaction(encounter, encounterId,
          claim, claimId, chargeId, claimEntry, rand);
      t.type = ClaimTransactionType.ADJUSTMENT;
      t.method = PaymentMethod.SYSTEM;
      t.adjustment = claimEntry.adjustment;
      t.unpaid = remainder;
      t.departmentId = departmentId;
      t.diagnosisCodes = diagnosisCodes;
      write(t.toString(), claimsTransactions);
      chargeId = transactionId.getAndIncrement();
    }

    BigDecimal payerAmount = claimEntry.paidByPayer.add(claimEntry.coinsurancePaidByPayer);
    if (payerAmount.compareTo(Claim.ZERO_CENTS) > 0) {
      // PAYMENT FROM INSURANCE
      remainder = remainder.subtract(payerAmount);
      t = new ClaimTransaction(encounter, encounterId,
          claim, claimId, chargeId, claimEntry, rand);
      t.type = ClaimTransactionType.PAYMENT;
      t.method = PaymentMethod.ECHECK;
      t.payment = payerAmount;
      t.unpaid = remainder;
      t.departmentId = departmentId;
      t.diagnosisCodes = diagnosisCodes;
      write(t.toString(), claimsTransactions);
      chargeId = transactionId.getAndIncrement();
    }

    BigDecimal secondaryPayerAmount = claimEntry.paidBySecondaryPayer;
    if (secondaryPayerAmount.compareTo(Claim.ZERO_CENTS) > 0) {
      // TRANSFEROUT
      t = new ClaimTransaction(encounter, encounterId,
          claim, claimId, chargeId, claimEntry, rand);
      t.type = ClaimTransactionType.TRANSFEROUT;
      t.amount = remainder;
      t.unpaid = remainder;
      t.departmentId = departmentId;
      t.diagnosisCodes = diagnosisCodes;
      write(t.toString(), claimsTransactions);
      long transferOut = chargeId;
      chargeId = transactionId.getAndIncrement();

      // TRANSFERIN
      t = new ClaimTransaction(encounter, encounterId,
          claim, claimId, chargeId, claimEntry, rand);
      t.type = ClaimTransactionType.TRANSFERIN;
      t.transferType = "2"; // "2" if secondary insurance
      t.transferId = transferOut;
      t.amount = remainder;
      t.unpaid = remainder;
      t.departmentId = departmentId;
      t.diagnosisCodes = diagnosisCodes;
      write(t.toString(), claimsTransactions);
      chargeId = transactionId.getAndIncrement();

      // PAYMENT FROM SECONDARY INSURANCE
      remainder = remainder.subtract(secondaryPayerAmount);

      // PAYMENT
      t = new ClaimTransaction(encounter, encounterId,
          claim, claimId, chargeId, claimEntry, rand);
      t.type = ClaimTransactionType.PAYMENT;
      t.method = PaymentMethod.ECHECK;
      t.payment = secondaryPayerAmount;
      t.unpaid = remainder;
      t.departmentId = departmentId;
      t.diagnosisCodes = diagnosisCodes;
      write(t.toString(), claimsTransactions);
      chargeId = transactionId.getAndIncrement();
    }

    if (remainder.compareTo(Claim.ZERO_CENTS) > 0) {
      if (claim.plan != PayerManager.getNoInsurancePlan()) {
        // TRANSFEROUT
        t = new ClaimTransaction(encounter, encounterId,
            claim, claimId, chargeId, claimEntry, rand);
        t.type = ClaimTransactionType.TRANSFEROUT;
        t.amount = remainder;
        t.unpaid = remainder;
        t.departmentId = departmentId;
        t.diagnosisCodes = diagnosisCodes;
        write(t.toString(), claimsTransactions);
        long transferOut = chargeId;
        chargeId = transactionId.getAndIncrement();

        // TRANSFERIN
        t = new ClaimTransaction(encounter, encounterId,
            claim, claimId, chargeId, claimEntry, rand);
        t.type = ClaimTransactionType.TRANSFERIN;
        t.transferType = "p"; // patient
        t.transferId = transferOut;
        t.amount = remainder;
        t.unpaid = remainder;
        t.departmentId = departmentId;
        t.diagnosisCodes = diagnosisCodes;
        write(t.toString(), claimsTransactions);
        chargeId = transactionId.getAndIncrement();
      }
      // PAYMENT
      t = new ClaimTransaction(encounter, encounterId,
          claim, claimId, chargeId, claimEntry, rand);
      t.type = ClaimTransactionType.PAYMENT;
      String[] opts = { PaymentMethod.CASH.toString(),
          PaymentMethod.CHECK.toString(),
          PaymentMethod.CC.toString()};
      t.method = PaymentMethod.valueOf(rand.rand(opts));
      t.payment = remainder;
      t.unpaid = Claim.ZERO_CENTS;
      t.departmentId = departmentId;
      t.diagnosisCodes = diagnosisCodes;
      write(t.toString(), claimsTransactions);
      chargeId = transactionId.getAndIncrement();
    }
  }

  public enum ClaimTransactionType {
    CHARGE, PAYMENT, ADJUSTMENT, TRANSFERIN, TRANSFEROUT;
  }

  public enum PaymentMethod {
    CASH, CHECK, COPAY, SYSTEM, CC, ECHECK;
  }

  public class ClaimTransaction {
    String id;
    String encounterId;
    String claimId;
    long chargeId;
    long transferId;
    String transferType;
    String patientId;
    String memberId;
    ClaimTransactionType type;
    BigDecimal amount = Claim.ZERO_CENTS;
    Integer units;
    BigDecimal unitAmount = Claim.ZERO_CENTS;
    BigDecimal payment = Claim.ZERO_CENTS;
    BigDecimal adjustment = Claim.ZERO_CENTS;
    BigDecimal unpaid = Claim.ZERO_CENTS;
    PaymentMethod method;
    long start;
    long stop;
    String organizationId;
    String departmentId;
    String clinicianId;
    String procedureCode;
    String procedureDisplay;
    String[] diagnosisCodes;
    String notes;

    /**
     * Create a new ClaimTransaction.
     * @param encounter The Encounter.
     * @param encounterId The Encounter ID.
     * @param claim The Claim.
     * @param claimId The Claim ID.
     * @param chargeId The Charge ID.
     * @param claimEntry The entry for the transactions.
     * @param rand A random number generator.
     */
    public ClaimTransaction(Encounter encounter, String encounterId, Claim claim, String claimId,
        long chargeId, Claim.ClaimEntry claimEntry, RandomNumberGenerator rand) {
      this.id = rand.randUUID().toString();
      this.encounterId = encounterId;
      this.claimId = claimId;
      this.chargeId = chargeId;
      this.patientId = (String) claim.person.attributes.get(Person.ID);
      PlanRecord plan = claim.person.coverage.getPlanRecordAtTime(encounter.start);
      if (plan != null) {
        this.memberId = plan.id;
      }
      this.units = 1;
      this.start = claimEntry.entry.start;
      this.stop = claimEntry.entry.stop;
      if (encounter.provider != null) {
        this.organizationId = encounter.provider.getResourceID();
      }
      if (encounter.clinician != null) {
        this.clinicianId = encounter.clinician.getResourceID();
      }
      this.procedureCode = clean(claimEntry.entry.codes.get(0).code);
      this.procedureDisplay = clean(claimEntry.entry.codes.get(0).display);
    }

    public void setAmount(BigDecimal amount) {
      this.amount = amount;
      this.unitAmount = amount;
    }

    /**
     * Convert this ClaimTransaction into a CSV row.
     */
    public String toString() {
      // ID,CLAIMID,CHARGEID,PATIENTID,TYPE,AMOUNT,METHOD,FROMDATE,TODATE,
      // PLACEOFSERVICE,PROCEDURECODE,MODIFIER1,MODIFIER2,DIAGNOSISREF1,DIAGNOSISREF2,
      // DIAGNOSISREF3,DIAGNOSISREF4,UNITS,DEPARTMENTID,NOTES,UNITAMOUNT,TRANSFEROUTID,
      // TRANSFERTYPE,PAYMENTS,ADJUSTMENTS,TRANSFERS,OUTSTANDING,APPOINTMENTID,LINENOTE,
      // PATIENTINSURANCEID,FEESCHEDULEID,PROVIDERID,SUPERVISINGPROVIDERID
      StringBuilder s = new StringBuilder();
      // ID
      s.append(id).append(',');
      // CLAIMID
      s.append(claimId).append(',');
      // CHARGEID
      s.append(chargeId).append(',');
      // PATIENTID
      s.append(patientId).append(',');
      // TYPE: CHARGE, PAYMENT, ADJUSTMENT, TRANSFERIN, TRANSFEROUT
      s.append(type.toString()).append(',');
      // AMOUNT
      if (type == ClaimTransactionType.CHARGE || type == ClaimTransactionType.TRANSFERIN) {
        s.append(String.format(Locale.US, "%.2f", amount));
      }
      s.append(',');
      // METHOD
      if (type == ClaimTransactionType.PAYMENT || type == ClaimTransactionType.ADJUSTMENT) {
        s.append(method);
      }
      s.append(',');
      // FROMDATE
      s.append(iso8601Timestamp(start)).append(',');
      // TODATE
      s.append(iso8601Timestamp(stop)).append(',');
      // PLACEOFSERVICE
      s.append(organizationId).append(',');
      // PROCEDURECODE
      s.append(procedureCode).append(',');
      // MODIFIER1
      s.append(',');
      // MODIFIER2
      s.append(',');
      // DIAGNOSISREF1, DIAGNOSISREF2, DIAGNOSISREF3, DIAGNOSISREF4
      // Diagnosis codes
      for (int i = 0; i < 4; i++) {
        String diagnosisCode = diagnosisCodes[i];
        if (diagnosisCode != null && !diagnosisCode.isEmpty()) {
          s.append(i + 1).append(','); // don't write the code, write the reference number
        } else {
          s.append(',');
        }
      }
      // UNITS
      if (units != null) {
        s.append(units);
      }
      s.append(',');
      // DEPARTMENTID
      s.append(departmentId).append(',');
      // NOTES
      s.append(clean(procedureDisplay)).append(',');
      // UNITAMOUNT
      if (unitAmount != null) {
        s.append(String.format(Locale.US, "%.2f", unitAmount));
      }
      s.append(',');
      // TRANSFEROUTID
      if (type == ClaimTransactionType.TRANSFERIN) {
        s.append(transferId);
      }
      s.append(',');
      // TRANSFERTYPE. 1=primary insurance, 2=secondary, p==patient
      if (type == ClaimTransactionType.CHARGE || type == ClaimTransactionType.TRANSFERIN) {
        s.append(transferType);
      }
      s.append(',');
      // PAYMENTS
      if (payment != null) {
        s.append(String.format(Locale.US, "%.2f", payment));
      }
      s.append(',');
      // ADJUSTMENTS
      if (adjustment != null) {
        s.append(String.format(Locale.US, "%.2f", adjustment));
      }
      s.append(',');
      // TRANSFERS
      if (type == ClaimTransactionType.TRANSFERIN || type == ClaimTransactionType.TRANSFEROUT) {
        s.append(String.format(Locale.US, "%.2f", amount));
      }
      s.append(',');
      // OUTSTANDING
      if (unpaid != null) {
        s.append(String.format(Locale.US, "%.2f", unpaid));
      }
      s.append(',');
      // APPOINTMENTID
      s.append(encounterId).append(',');
      // LINENOTE
      s.append(',');
      // PATIENTINSURANCEID
      if (memberId != null) {
        s.append(patientId);
      }
      s.append(',');
      // TODO FEESCHEDULEID
      s.append("1").append(',');
      // PROVIDERID
      s.append(clinicianId).append(',');
      // SUPERVISINGPROVIDERID
      s.append(clinicianId);
      s.append(NEWLINE);
      return s.toString();
    }
  }

  /**
   * Replaces commas and line breaks in the source string with a single space.
   * Null is replaced with the empty string.
   */
  private static String clean(String src) {
    if (src == null) {
      return "";
    } else {
      return src.replaceAll("\\r\\n|\\r|\\n|,", " ").trim();
    }
  }

  /**
   * Helper method to write a line to a File. Extracted to a separate method here
   * to make it a little easier to replace implementations.
   *
   * @param line   The line to write
   * @param writer The place to write it
   * @throws IOException if an I/O error occurs
   */
  private static void write(String line, OutputStreamWriter writer) throws IOException {
    synchronized (writer) {
      writer.write(line);
    }
  }

  /**
   * "No-op" writer to use to prevent writing to excluded files.
   * Note that this uses an Apache "NullOutputStream", but JDK11 provides its own.
   */
  private static final OutputStreamWriter NO_OP =
      new OutputStreamWriter(NullOutputStream.NULL_OUTPUT_STREAM);

  /**
   * Helper method to get the writer for the given output file.
   * Returns a "no-op" writer for any excluded files.
   *
   * @param outputDirectory Parent directory for output csv files
   * @param filename Filename for the current file
   * @param append True = append to an existing file, False = overwrite any existing files
   * @param includedFiles List of filenames that should be included in output
   * @param excludedFiles List of filenames that should not be included in output
   *
   * @return OutputStreamWriter for the given output file.
   */
  private OutputStreamWriter getWriter(Path outputDirectory, String filename, boolean append,
      List<String> includedFiles, List<String> excludedFiles) throws IOException {

    boolean excluded = (!includedFiles.isEmpty() && !includedFiles.contains(filename))
        || excludedFiles.contains(filename);
    if (excluded) {
      return NO_OP;
    }

    File file = outputDirectory.resolve(filename).toFile();
    // file writing may fail if we tell it to append to a file that doesn't already exist
    append = append && file.exists();
    return new OutputStreamWriter(new FileOutputStream(file, append), charset);
  }
}


text:
package com.capitalone.dashboard.model;

/**
 * Denotes the status of a product or service
 */
public enum ServiceStatus {
    Ok(200, 200),
    Warning(300, 400),
    Unauth(401, 401),
    Alert(0, 999);

	private int low;
	private int high;

	private ServiceStatus(int low, int high) {
		this.low = low;
		this.high = high;
	}

	public static ServiceStatus fromString(String value) {
        for(ServiceStatus status : ServiceStatus.values()) {
            if (status.toString().equalsIgnoreCase(value)) {
                return status;
            }
        }
        throw new IllegalArgumentException(value + " is not a valid ServiceStatus.");
    }

	public static ServiceStatus getServiceStatus(int statusCode) {
		for(ServiceStatus status : ServiceStatus.values()) {
			if(status.low <= statusCode && status.high >= statusCode) {
				return status;
			}
		}

		return Alert;
	}

}

redpajama_set_name: RedPajamaGithub


text:
package topologymanager

import (
	"k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/kubernetes/pkg/kubelet/cm/containermap"
	"reflect"
	"testing"
)

func TestGetAffinity(t *testing.T) {
	tcases := []struct {
		name          string
		containerName string
		podUID        string
		expected      TopologyHint
	}{
		{
			name:          "case1",
			containerName: "nginx",
			podUID:        "0aafa4c4-38e8-11e9-bcb1-a4bf01040474",
			expected:      TopologyHint{},
		},
	}
	for _, tc := range tcases {
		scope := scope{}
		actual := scope.GetAffinity(tc.podUID, tc.containerName)
		if !reflect.DeepEqual(actual, tc.expected) {
			t.Errorf("Expected Affinity in result to be %v, got %v", tc.expected, actual)
		}
	}
}

func TestAddContainer(t *testing.T) {
	testCases := []struct {
		name        string
		containerID string
		podUID      types.UID
	}{
		{
			name:        "Case1",
			containerID: "nginx",
			podUID:      "0aafa4c4-38e8-11e9-bcb1-a4bf01040474",
		},
		{
			name:        "Case2",
			containerID: "Busy_Box",
			podUID:      "b3ee37fc-39a5-11e9-bcb1-a4bf01040474",
		},
	}
	scope := scope{}
	scope.podMap = containermap.NewContainerMap()
	for _, tc := range testCases {
		pod := v1.Pod{}
		pod.UID = tc.podUID
		container := v1.Container{}
		container.Name = tc.name
		scope.AddContainer(&pod, &container, tc.containerID)
		if val, ok := scope.podMap[tc.containerID]; ok {
			if reflect.DeepEqual(val, pod.UID) {
				t.Errorf("Error occurred")
			}
		} else {
			t.Errorf("Error occurred, Pod not added to podMap")
		}
	}
}

func TestRemoveContainer(t *testing.T) {
	testCases := []struct {
		name        string
		containerID string
		podUID      types.UID
	}{
		{
			name:        "Case1",
			containerID: "nginx",
			podUID:      "0aafa4c4-38e8-11e9-bcb1-a4bf01040474",
		},
		{
			name:        "Case2",
			containerID: "Busy_Box",
			podUID:      "b3ee37fc-39a5-11e9-bcb1-a4bf01040474",
		},
	}
	var len1, len2 int
	var lenHints1, lenHints2 int
	scope := scope{}
	scope.podMap = containermap.NewContainerMap()
	scope.podTopologyHints = podTopologyHints{}
	for _, tc := range testCases {
		scope.podMap.Add(string(tc.podUID), tc.name, tc.containerID)
		scope.podTopologyHints[string(tc.podUID)] = make(map[string]TopologyHint)
		scope.podTopologyHints[string(tc.podUID)][tc.name] = TopologyHint{}
		len1 = len(scope.podMap)
		lenHints1 = len(scope.podTopologyHints)
		err := scope.RemoveContainer(tc.containerID)
		len2 = len(scope.podMap)
		lenHints2 = len(scope.podTopologyHints)
		if err != nil {
			t.Errorf("Expected error to be nil but got: %v", err)
		}
		if len1-len2 != 1 {
			t.Errorf("Remove Pod from podMap resulted in error")
		}
		if lenHints1-lenHints2 != 1 {
			t.Error("Remove Pod from podTopologyHints resulted in error")
		}
	}

}


text:
package org.conqat.engine.html_presentation.util;

import static org.conqat.lib.commons.html.EHTMLElement.BR;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.conqat.engine.commons.findings.EFindingDeltaState;
import org.conqat.engine.commons.findings.EFindingKeys;
import org.conqat.engine.commons.findings.Finding;
import org.conqat.engine.commons.findings.FindingsList;
import org.conqat.engine.commons.findings.location.ElementLocation;
import org.conqat.engine.commons.findings.location.QualifiedNameLocation;
import org.conqat.engine.commons.findings.location.TextRegionLocation;
import org.conqat.engine.commons.node.IConQATNode;
import org.conqat.engine.commons.node.NodeUtils;
import org.conqat.engine.html_presentation.layouters.TableLayouter;
import org.conqat.engine.html_presentation.util.PresentationUtils.IContextSensitiveFormatter;
import org.conqat.engine.resource.IElement;
import org.conqat.engine.resource.text.ITextElement;
import org.conqat.lib.commons.html.EHTMLAttribute;
import org.conqat.lib.commons.html.EHTMLElement;
import org.conqat.lib.commons.html.HTMLWriter;
import org.conqat.lib.commons.string.StringUtils;

/**
 * Base class for node aware formatters dealing with formatting of findings.
 * This class has a notion of a local node, which is the node for which the
 * findings are formatted.
 *
 * @author $Author: hummelb $
 * @version $Rev: 43290 $
 * @ConQAT.Rating GREEN Hash: CF1879957466DBF8E97A00B39B2E7446
 */
public abstract class NodeAwareFindingFormatterBase implements
		IContextSensitiveFormatter<FindingsList> {

	/** Uniform path of local node. */
	protected final String uniformPath;

	/** The element the findings belong to (or null). */
	protected final ITextElement element;

	/** Constructor. */
	protected NodeAwareFindingFormatterBase(IConQATNode localNode) {
		if (localNode instanceof IElement) {
			uniformPath = ((IElement) localNode).getUniformPath();
			if (localNode instanceof ITextElement) {
				element = (ITextElement) localNode;
			} else {
				element = null;
			}
		} else {
			uniformPath = StringUtils.EMPTY_STRING;
			element = null;
		}
	}

	/** {@inheritDoc} */
	@Override
	public boolean isApplicable(Object value) {
		return value instanceof FindingsList;
	}

	/** {@inheritDoc} */
	@Override
	public void formatObject(FindingsList findings, HTMLWriter writer) {

		List<StructuredFinding> structuredFindings = new ArrayList<StructuredFinding>();
		for (Finding finding : findings) {
			structuredFindings.add(new StructuredFinding(finding));
		}
		Collections.sort(structuredFindings);

		boolean needsBreak = false;
		for (StructuredFinding structuredFinding : structuredFindings) {
			if (needsBreak) {
				writer.addClosedElement(BR);
			}
			needsBreak = true;

			structuredFinding.format(writer);
		}
	}

	/**
	 * Formats a location in a local fashion (i.e. without including the known
	 * uniform path). May return null if the location can not be more precise
	 * than the local node (e.g. if the finding affects the entire element).
	 */
	protected String formatLocalLocation(ElementLocation location) {
		if (location instanceof TextRegionLocation) {
			TextRegionLocation regionLocation = (TextRegionLocation) location;
			return "lines " + regionLocation.getRawStartLine() + " - "
					+ regionLocation.getRawEndLine();
		}

		if (location instanceof QualifiedNameLocation) {
			return ((QualifiedNameLocation) location).getQualifiedName();
		}

		return null;
	}

	/** Returns the URL to be used for the given location (or null). */
	protected abstract String determineUrl(ElementLocation location);

	/** This class prestructures the locations of a finding. */
	private class StructuredFinding implements Comparable<StructuredFinding> {

		/** The finding. */
		private final Finding finding;

		/** Constructor. */
		public StructuredFinding(Finding finding) {
			this.finding = finding;
		}

		/** Formats this finding into the given writer. */
		public void format(HTMLWriter writer) {
			String url = determineUrl(finding.getLocation());

			if (url != null) {
				writer.openElement(EHTMLElement.A, EHTMLAttribute.HREF, url,
						EHTMLAttribute.CLASS, TableLayouter.LINK_CLASS);
			}

			if (EFindingDeltaState.isInState(finding, EFindingDeltaState.ADDED)) {
				writer.addText("+ ");
			}

			if (EFindingDeltaState.isInState(finding,
					EFindingDeltaState.IN_MODIFIED_CODE)) {
				writer.addText("* ");
			}

			String position = formatLocalLocation(finding.getLocation());
			if (position != null) {
				writer.addText(position + ": ");
			}
			writer.addText(getMessage());

			if (url != null) {
				writer.closeElement(EHTMLElement.A);
			}

			if (finding.getValue(EFindingKeys.FINGERPRINT.name()) != null) {
				writer.addText(" ");
				writer.openElement(
						EHTMLElement.A,
						EHTMLAttribute.ONCLICK,
						"prompt('Hash for finding is:','"
								+ finding.getValue("fingerprint") + "')",
						EHTMLAttribute.CLASS, TableLayouter.LINK_CLASS);
				writer.addText("#");
				writer.closeElement(EHTMLElement.A);
			}
		}

		/** Returns the message used. */
		private String getMessage() {
			// fallback to group name for missing message
			return NodeUtils.getStringValue(finding, EFindingKeys.MESSAGE
					.toString(), finding.getParent().getName());
		}

		/**
		 * {@inheritDoc}
		 * <p>
		 * Basically sorts by location.
		 */
		@SuppressWarnings("unchecked")
		@Override
		public int compareTo(StructuredFinding other) {
			int primaryKey1 = getPrimarySortKey();
			int primaryKey2 = other.getPrimarySortKey();
			if (primaryKey1 != primaryKey2) {
				return primaryKey1 - primaryKey2;
			}

			Comparable<Object> secondaryKey1 = (Comparable<Object>) getSecondaryKey();
			Comparable<Object> secondaryKey2 = (Comparable<Object>) other
					.getSecondaryKey();
			int compare = secondaryKey1.compareTo(secondaryKey2);
			if (compare != 0) {
				return compare;
			}

			return getMessage().compareTo(other.getMessage());
		}

		/**
		 * Returns a primary sorting key. This orders findings by kind of
		 * location.
		 */
		private int getPrimarySortKey() {
			if (finding.getLocation() instanceof QualifiedNameLocation) {
				return 2;
			}
			if (finding.getLocation() instanceof TextRegionLocation) {
				return 3;
			}
			return 1;
		}

		/**
		 * Returns a secondary sorting key. This only has to be comparable for
		 * findings where {@link #getPrimarySortKey()} returns the same value.
		 */
		private Comparable<?> getSecondaryKey() {
			if (finding.getLocation() instanceof QualifiedNameLocation) {
				return ((QualifiedNameLocation) finding.getLocation())
						.getQualifiedName();
			}
			if (finding.getLocation() instanceof TextRegionLocation) {
				return ((TextRegionLocation) finding.getLocation())
						.getRawStartOffset();
			}

			// sort others by message
			return getMessage();
		}
	}

}


text:
using namespace ABI::Windows::Storage;
using namespace Microsoft::WRL;
using namespace Windows::Foundation;

Wrappers::HString GetAppDataPath() {
    Wrappers::HString toReturn;
    ComPtr<IStorageFolder> folder;
    ComPtr<IApplicationDataStatics> applicationDataStatics;
    ComPtr<IApplicationData> applicationData;
    ComPtr<IStorageItem> storageItem;

    if (FAILED(GetActivationFactory(Wrappers::HStringReference(RuntimeClass_Windows_Storage_ApplicationData).Get(),
                                    &applicationDataStatics))) {
        return toReturn;
    }

    if (FAILED(applicationDataStatics->get_Current(&applicationData))) {
        return toReturn;
    }

    if (FAILED(applicationData->get_LocalFolder(&folder))) {
        return toReturn;
    }

    if (FAILED(folder.As<IStorageItem>(&storageItem))) {
        return toReturn;
    }

    if (FAILED(storageItem->get_Path(toReturn.GetAddressOf()))) {
        return toReturn;
    }

    return toReturn;
}

using namespace benchmark;
struct __Result {
    std::string testName;
    Microseconds totalRuntime;
    Microseconds minRuntime;
    Microseconds meanRuntime;
    Microseconds medianRuntime;
    Microseconds stdDeviation;
    size_t runCount;
};

class PublisherBase : public BenchmarkPublisher {
public:
    // Expect results.size() > 1
    virtual void RegisterCaseResults(const std::string& caseName, std::vector<Microseconds>& results) {
        // Algorithm for calculating sample mean and variance with minimal rounding errors
        Microseconds netRuntime = 0.0;
        Microseconds minRuntime = FLT_MAX;
        Microseconds mean = 0.0;
        Microseconds q = 0.0;
        size_t runCount = results.size();
        for (size_t i = 0; i < runCount; ++i) {
            Microseconds previousMean = mean;
            minRuntime = min(minRuntime, results[i]);
            netRuntime += results[i];
            mean += (results[i] - mean) / (i + 1);
            q += (results[i] - previousMean) * (results[i] - mean);
        }

        Microseconds sampleStandardDeviation = std::sqrt(q / (runCount - 1));
        std::nth_element(results.begin(), results.begin() + runCount / 2, results.end());
        Microseconds median = results[runCount / 2];

        if ((runCount & 1) == 0) {
            // Median is the mean of the two central elements
            std::nth_element(results.begin(), results.begin() + (runCount / 2) - 1, results.end());
            median = (median + results[(runCount / 2) - 1]) / 2.0;
        }

        m_results.emplace_back(__Result{ caseName, netRuntime, minRuntime, mean, median, sampleStandardDeviation, runCount });
    }

protected:
    std::vector<__Result> m_results;
};

class CSVBenchmarkPublisher : public PublisherBase {
    std::wstring m_outPath;

public:
    CSVBenchmarkPublisher(std::wstring&& outPath) : m_outPath(outPath) {
    }

    virtual void PublishResults() {
        std::ofstream outFile(m_outPath);
        outFile << "Case Name"
                << ","
                << "Total Runtime"
                << ","
                << "Min Runtime"
                << ","
                << "Mean Runtime"
                << ","
                << "Median Runtime"
                << ","
                << "Standard Deviation"
                << ","
                << "Run Count" << std::endl;
        for (auto res : m_results) {
            outFile << res.testName << "," << res.totalRuntime << "," << res.minRuntime << "," << res.meanRuntime << ","
                    << res.medianRuntime << "," << res.stdDeviation << "," << res.runCount << std::endl;
        }

        outFile.flush();
        outFile.close();

        // Actually saves the file since this is running as TAEF
        WEX::Logging::Log::File(m_outPath.c_str());
        m_results.clear();
    }
};

static std::shared_ptr<BenchmarkPublisher> s_publisher;
void BenchmarkPublisherFactory::CreatePublisher(int argc, char** argv) {
    // Currently only support CSV format
    static std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter;

    WEX::Common::String outputPath;
    WEX::TestExecution::RuntimeParameters::TryGetValue(L"output", outputPath);
    if (!outputPath.IsEmpty()) {
        s_publisher.reset(new CSVBenchmarkPublisher(std::wstring(outputPath)));
    }

    if (!s_publisher) {
        WEX::Logging::Log::Comment(L"Using default output filename");
        s_publisher.reset(new CSVBenchmarkPublisher(std::wstring(L"benchmark_results.csv")));
    }
}

std::shared_ptr<BenchmarkPublisher> BenchmarkPublisherFactory::GetPublisher() {
    return s_publisher;
}

BEGIN_MODULE()
MODULE_PROPERTY(L"RunAs", L"UAP")
MODULE_PROPERTY(L"UAP:AppXManifest", L"Default.AppxManifest.xml")
END_MODULE()

MODULE_SETUP(ModuleSetup) {
    if (FAILED(RoInitialize(RO_INIT_MULTITHREADED))) {
        return FALSE;
    }

    Wrappers::HString path = GetAppDataPath();
    if (path.IsValid()) {
        unsigned int rawLength;
        _wchdir(WindowsGetStringRawBuffer(path.Get(), &rawLength));
    }

    int argc = 1;
    char* argv[] = { "UnitTests" };

    BenchmarkPublisherFactory::CreatePublisher(argc, argv);
    testing::InitGoogleTest(&argc, argv);
    return TRUE;
}

MODULE_CLEANUP(ModuleCleanup) {
    BenchmarkPublisherFactory::GetPublisher()->PublishResults();
    RoUninitialize();
    return true;
}


text:
function getDistance(draggable) {
    // calculate location and angle relative to center of control based on mouse position during drag (leaflet class)
    var rect = draggable._dragStartTarget.getBoundingClientRect(),
        centerX = (rect.left + rect.right) / 2,
        centerY = (rect.top + rect.bottom) / 2,
        offsetX = event ? event.clientX - centerX : draggable._lastEvent.clientX - centerX,
        offsetY = event ? event.clientY - centerY : draggable._lastEvent.clientY - centerY;

    draggable.dx = offsetX * 2 / rect.width;
    draggable.dy = offsetY * 2 / rect.height;
    // distance to center
    draggable.d = Math.sqrt(draggable.dx * draggable.dx + draggable.dy * draggable.dy);
    // angle in degrees to center [0..360]
    draggable.r = (Math.atan2(draggable.dy, draggable.dx) * (180 / Math.PI) + 270) % 360;
    // speed, [0..1] relative to distance to center (1=0.7 of ellipse filling control) logarithmic scale (Niek)
    draggable.s = Math.pow(Math.min(draggable.d / 0.7, 1.0), 2.5) * draggable.ref.options.maxSpeed;
    // return the relative distance to center of control
    return draggable.d;
}

// define control type
L.Control.Arrow = L.Control.extend({
    arrow: null,
    text: null,
    active: false,
    currentData: { speed: 0, direction: 0 },

    arrowImageRotationCompensation: 180,
    arrowImagePath: "Content/images/arrow_wind.png",
    arrowImageManualPath: "Content/images/arrow_wind_manual.png",

    options: {
        collapsed: false,
        position: 'topleft',
        autoZIndex: true,
        height: 150,
        width: 150,
        maxSpeed: 40 //in m/s
    },

    /// leaflet constructor
    initialize: function (options) {
        L.setOptions(this, options);
    },

    onAdd: function (map) {
        this.active = true;
        this._initLayout();
        this._map = map;
        this._update();
        return this._container;
    },

    onRemove: function () {
        this.active = false;
    },

    _initLayout: function () {
        // main div
        this._container = L.DomUtil.create('div', 'leaflet-control-windDirection');
        // add drag support to main div
        this._draggable = new L.Draggable(this._container);
        this._draggable.ref = this;
        this._draggable._originalupdatePosition = this._draggable._updatePosition;
        this._draggable._updatePosition = function (e) {
            // action is set in _dragdown
            // check if we are ragging main div or are setting wind direction or speed
            if (this.action) {
                if (this.action > 0) {
                    var d = getDistance(this);
                    switch (this.action) {
                        // setting speed
                        case 1:
                            this.ref.currentData.speed = this.s;
                            this.ref.rotate(this.ref.currentData.direction, this.ref.currentData.speed);
                            break;
                        // setting wind direction
                        case 2:
                            this.ref.currentData.direction = this.r;
                            this.ref.rotate(this.ref.currentData.direction, this.ref.currentData.speed);
                            break;
                    }
                    // work-a-round: ensure dragend is fired!
                    this._moved = true;
                    this._moving = true;
                }
                else {
                    // ..

                }
            }
            else {
                // we are dragging the main div (control as a whole)
                this._originalupdatePosition(e);
            }
        };
        this._draggable.enable();
        this._draggable.addEventListener('down', this._dragdown);
        this._draggable._onUp = (function (e) {
            if (e._simulated || !this._enabled) { return; }
            this.finishDrag();
            this.ref._dragup();
        }).bind(this._draggable); // work-a-round, no up event is fired

        L.DomEvent.disableClickPropagation(this._container);
        L.DomEvent.disableScrollPropagation(this._container);
        // use context menu event to return to "live" state
        this._container.addEventListener('contextmenu', this._handleContextMenu.bind(this));

        this.text = this._container.appendChild(L.DomUtil.create('span', "windDirectionText"));
        this.text.innerHTML = "
unknown
";
        this._windDirectionDiv = L.DomUtil.create('div', "windDirectionDiv");
        this._windDirectionDiv.appendChild(this.text);
        this._container.appendChild(this._windDirectionDiv);
        this._windDirectionDiv.style.width = this.options.width + 'px';
        this._windDirectionDiv.style.height = this.options.height + 'px';
        this._windDirectionDiv.appendChild(L.DomUtil.create('div', "north"))
            .innerHTML = 'N';
        this._windDirectionDiv.appendChild(L.DomUtil.create('div', "south"))
            .innerHTML = 'S';
        this._windDirectionDiv.appendChild(L.DomUtil.create('div', "east"))
            .innerHTML = 'E';
        this._windDirectionDiv.appendChild(L.DomUtil.create('div', "west"))
            .innerHTML = 'W';
        this.arrow = this._windDirectionDiv.appendChild(L.DomUtil.create('img', "arrowImage"));
        this.arrow.src = "Content/images/arrow_wind.png";
        this.arrow.id = "windArrow";
        L.DomUtil.addClass(this.arrow, 'windArrowLive');
        // show initial direction and speed
        this.rotate(this.currentData.direction, this.currentData.speed);
    },

    _handleContextMenu: function (e) {
        e.preventDefault();
        e.cancelBubble = true;

        // return to live state
        this.arrow.src = "Content/images/arrow_wind.png";
        wsSend({
            type: 'windData',
            payload: { live: true }
        });
    },

    _dragdown: function (e) {
        // check if we are going to drag the main div or set the wind direction or set the wind speed
        // set action accordingly to process action in
        if (event && event.button != 2) {
            var d = getDistance(this);
            if (this.d < 1.0) {
                var angleDiff = (this.r - this.ref.currentData.direction + 180 + 360) % 360 - 180;
                if (this.d < 0.7 && -10 <= angleDiff && angleDiff <= 10) {
                    // setting wind speed
                    this.action = 1;
                    // move to selected point
                    this.ref.currentData.speed = this.s;
                    this.ref.rotate(this.ref.currentData.direction, this.ref.currentData.speed);
                }
                else {
                    // rotating
                    this.action = 2;
                    // move to selected point
                    this.ref.currentData.direction = this.r;
                    this.ref.rotate(this.ref.currentData.direction, this.ref.currentData.speed);
                }
                // not-live
                this.ref.arrow.src = "Content/images/arrow_wind_manual.png";
                wsSend({
                    type: 'windData',
                    payload: { live: false }
                });
            }
            else {
                // nothing, just dragging
                this.action = 0;
            }
        }
        else
            this.action = -1;
    },

    _dragup: function (e) {
        // end the drag action according to the previously set action
        switch (this._draggable.action) {
            case 1:
                // final speed
                this.currentData.speed = this._draggable.s;
                this.rotate(this.currentData.direction, this.currentData.speed);
                // send new speed
                wsSend({
                    type: 'windData',
                    payload: { speed: this.currentData.speed }
                });
                break;
            case 2:
                // final rotate
                this.currentData.direction = this._draggable.r;
                this.rotate(this.currentData.direction, this.currentData.speed);
                // send new direction
                wsSend({
                    type: 'windData',
                    payload: { direction: this.currentData.direction }
                });
                break;
        }
    },

    _update: function () {
        // todo: needed?
    },

    NewData: (function (data) {
        // set new state via websocket
        // show new live state
        this.arrow.src = data.live ? "Content/images/arrow_wind.png" : "Content/images/arrow_wind_manual.png";
        // speed and/or direction
        var changed = false;
        if (typeof data.speed !== 'undefined' && data.speed != this.currentData.speed) {
            this.currentData.speed = data.speed;
            changed = true;
        }
        if (typeof data.direction !== 'undefined' && data.direction != this.currentData.direction) {
            this.currentData.direction = data.direction;
            changed = true;
        }
        // show new state of speed and direction if any changed
        if (changed)
            this.rotate(this.currentData.direction, this.currentData.speed);
    }),

    rotate: function (degrees, speed) {
        // show wind direction and speed
        degrees = Math.round(degrees * 100) / 100;
        speed = Math.round(speed * 100) / 100;
        // according to position of wind vane position text with speed and direction over or below wind vane
        if (degrees >= 270 || degrees <= 90) {
            this.text.style.top = '20%';
            this.text.style.removeProperty('bottom');
        } else {
            this.text.style.bottom = '20%';
            this.text.style.removeProperty('top');
        }
        // update visual
        this.arrow.style.transform = "rotate(" + (degrees + 90) + "deg)";
        this.arrow.style.transform.webkitTransform = "rotate(" + (degrees + 90) + "deg)";
        this.text.innerHTML = degrees + '°' + "
" + speed + ' m/s';
    }
});

// add arrow constructor for arrow control
L.control.arrow = function () {
    return new L.Control.Arrow();
};