// Copyright (C) 2014 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "validation_task.h"

#include <libaddressinput/address_data.h>
#include <libaddressinput/address_field.h>
#include <libaddressinput/address_metadata.h>
#include <libaddressinput/address_problem.h>
#include <libaddressinput/address_validator.h>
#include <libaddressinput/callback.h>
#include <libaddressinput/supplier.h>

#include <algorithm>
#include <cassert>
#include <cstddef>
#include <string>

#include <re2/re2.h>

#include "lookup_key.h"
#include "post_box_matchers.h"
#include "rule.h"
#include "util/re2ptr.h"
#include "util/size.h"

namespace i18n {
namespace addressinput {

ValidationTask::ValidationTask(const AddressData& address, bool allow_postal,
                               bool require_name, const FieldProblemMap* filter,
                               FieldProblemMap* problems,
                               const AddressValidator::Callback& validated)
    : address_(address),
      allow_postal_(allow_postal),
      require_name_(require_name),
      filter_(filter),
      problems_(problems),
      validated_(validated),
      supplied_(BuildCallback(this, &ValidationTask::Validate)),
      lookup_key_(new LookupKey),
      max_depth_(size(LookupKey::kHierarchy)) {
  assert(problems_ != nullptr);
  assert(supplied_ != nullptr);
  assert(lookup_key_ != nullptr);
}

ValidationTask::~ValidationTask() = default;

void ValidationTask::Run(Supplier* supplier) {
  assert(supplier != nullptr);
  problems_->clear();
  lookup_key_->FromAddress(address_);
  max_depth_ = supplier->GetLoadedRuleDepth(lookup_key_->ToKeyString(0));
  supplier->SupplyGlobally(*lookup_key_, *supplied_);
}

void ValidationTask::Validate(bool success,
                              const LookupKey& lookup_key,
                              const Supplier::RuleHierarchy& hierarchy) {
  assert(&lookup_key == lookup_key_.get());  // Sanity check.

  if (success) {
    if (address_.IsFieldEmpty(COUNTRY)) {
      ReportProblemMaybe(COUNTRY, MISSING_REQUIRED_FIELD);
    } else if (hierarchy.rule[0] == nullptr) {
      ReportProblemMaybe(COUNTRY, UNKNOWN_VALUE);
    } else {
      // Checks which use statically linked metadata.
      const std::string& region_code = address_.region_code;
      CheckUnexpectedField(region_code);
      CheckMissingRequiredField(region_code);

      // Checks which use data from the metadata server. Note that
      // CheckPostalCodeFormatAndValue assumes CheckUnexpectedField has already
      // been called.
      CheckUnknownValue(hierarchy);
      CheckPostalCodeFormatAndValue(hierarchy);
      CheckUsesPoBox(hierarchy);
      CheckUnsupportedField();
    }
  }

  validated_(success, address_, *problems_);
  delete this;
}

// A field will return an UNEXPECTED_FIELD problem type if the current value of
// that field is not empty and the field should not be used by that region.
void ValidationTask::CheckUnexpectedField(
    const std::string& region_code) const {
  static const AddressField kFields[] = {
      // COUNTRY is never unexpected.
      ADMIN_AREA,
      LOCALITY,
      DEPENDENT_LOCALITY,
      SORTING_CODE,
      POSTAL_CODE,
      STREET_ADDRESS,
      ORGANIZATION,
      RECIPIENT,
  };

  for (AddressField field : kFields) {
    if (!address_.IsFieldEmpty(field) && !IsFieldUsed(field, region_code)) {
      ReportProblemMaybe(field, UNEXPECTED_FIELD);
    }
  }
}

// A field will return an MISSING_REQUIRED_FIELD problem type if the current
// value of that field is empty and the field is required by that region.
void ValidationTask::CheckMissingRequiredField(
    const std::string& region_code) const {
  static const AddressField kFields[] = {
      // COUNTRY is assumed to have already been checked.
      ADMIN_AREA,
      LOCALITY,
      DEPENDENT_LOCALITY,
      SORTING_CODE,
      POSTAL_CODE,
      STREET_ADDRESS,
      // ORGANIZATION is never required.
      // RECIPIENT is handled separately.
  };

  for (AddressField field : kFields) {
    if (address_.IsFieldEmpty(field) && IsFieldRequired(field, region_code)) {
      ReportProblemMaybe(field, MISSING_REQUIRED_FIELD);
    }
  }

  if (require_name_ && address_.IsFieldEmpty(RECIPIENT)) {
    ReportProblemMaybe(RECIPIENT, MISSING_REQUIRED_FIELD);
  }
}

// A field is UNKNOWN_VALUE if the metadata contains a list of possible values
// for the field and the address data server could not match the current value
// of that field to one of those possible values, therefore returning nullptr.
void ValidationTask::CheckUnknownValue(
    const Supplier::RuleHierarchy& hierarchy) const {
  for (size_t depth = 1; depth < size(LookupKey::kHierarchy); ++depth) {
    AddressField field = LookupKey::kHierarchy[depth];
    if (!(address_.IsFieldEmpty(field) ||
          hierarchy.rule[depth - 1] == nullptr ||
          hierarchy.rule[depth - 1]->GetSubKeys().empty() ||
          hierarchy.rule[depth] != nullptr)) {
      ReportProblemMaybe(field, UNKNOWN_VALUE);
    }
  }
}

void ValidationTask::CheckUnsupportedField() const {
  for (size_t depth = max_depth_; depth < size(LookupKey::kHierarchy);
       ++depth) {
    ReportProblemMaybe(LookupKey::kHierarchy[depth], UNSUPPORTED_FIELD);
  }
}

// Note that it is assumed that CheckUnexpectedField has already been called.
void ValidationTask::CheckPostalCodeFormatAndValue(
    const Supplier::RuleHierarchy& hierarchy) const {
  assert(hierarchy.rule[0] != nullptr);
  const Rule& country_rule = *hierarchy.rule[0];

  if (!(ShouldReport(POSTAL_CODE, INVALID_FORMAT) ||
        ShouldReport(POSTAL_CODE, MISMATCHING_VALUE))) {
    return;
  }

  if (address_.IsFieldEmpty(POSTAL_CODE)) {
    return;
  } else if (std::find(problems_->begin(), problems_->end(),
                       FieldProblemMap::value_type(POSTAL_CODE,
                                                   UNEXPECTED_FIELD))
             != problems_->end()) {
    return;  // Problem already reported.
  }

  // Validate general postal code format. A country-level rule specifies the
  // regular expression for the whole postal code.
  const RE2ptr* format_ptr = country_rule.GetPostalCodeMatcher();
  if (format_ptr != nullptr &&
      !RE2::FullMatch(address_.postal_code, *format_ptr->ptr) &&
      ShouldReport(POSTAL_CODE, INVALID_FORMAT)) {
    ReportProblem(POSTAL_CODE, INVALID_FORMAT);
    return;
  }

  if (!ShouldReport(POSTAL_CODE, MISMATCHING_VALUE)) {
    return;
  }

  for (size_t depth = size(LookupKey::kHierarchy) - 1; depth > 0; --depth) {
    if (hierarchy.rule[depth] != nullptr) {
      // Validate sub-region specific postal code format. A sub-region specifies
      // the regular expression for a prefix of the postal code.
      const RE2ptr* prefix_ptr = hierarchy.rule[depth]->GetPostalCodeMatcher();
      if (prefix_ptr != nullptr) {
        if (!RE2::PartialMatch(address_.postal_code, *prefix_ptr->ptr)) {
          ReportProblem(POSTAL_CODE, MISMATCHING_VALUE);
        }
        return;
      }
    }
  }
}

void ValidationTask::CheckUsesPoBox(
    const Supplier::RuleHierarchy& hierarchy) const {
  assert(hierarchy.rule[0] != nullptr);
  const Rule& country_rule = *hierarchy.rule[0];

  if (allow_postal_ ||
      !ShouldReport(STREET_ADDRESS, USES_P_O_BOX) ||
      address_.IsFieldEmpty(STREET_ADDRESS)) {
    return;
  }

  const auto matchers = PostBoxMatchers::GetMatchers(country_rule);
  for (const auto& line : address_.address_line) {
    for (auto ptr : matchers) {
      assert(ptr != nullptr);
      if (RE2::PartialMatch(line, *ptr->ptr)) {
        ReportProblem(STREET_ADDRESS, USES_P_O_BOX);
        return;
      }
    }
  }
}

void ValidationTask::ReportProblem(AddressField field,
                                   AddressProblem problem) const {
  problems_->emplace(field, problem);
}

void ValidationTask::ReportProblemMaybe(AddressField field,
                                        AddressProblem problem) const {
  if (ShouldReport(field, problem)) {
    ReportProblem(field, problem);
  }
}

bool ValidationTask::ShouldReport(AddressField field,
                                  AddressProblem problem) const {
  return filter_ == nullptr || filter_->empty() ||
         std::find(filter_->begin(),
                   filter_->end(),
                   FieldProblemMap::value_type(field, problem)) !=
             filter_->end();
}

}  // namespace addressinput
}  // namespace i18n
