/// Validate a value against a schema defined as a Sass map.
///
/// @group schema
///
/// @parameter {Any}    $value  - any value
/// @parameter {Map}    $schema - a Sass schema
/// @parameter {String} $module - name of caller module, for error reporting
/// @parameter {String} $path   - validated variable name, for error reporting
///
/// @return {Boolean}
@function k-validate($value, $schema, $module, $path: 'root') {
  // Start by checking what type we expect for the current value.
  $value-type: type-of($value);
  $expected-type: map-get($schema, 'type');

  @if $expected-type == 'number' {
    @if $value-type != 'number' {
      @return k-schema-error(
        $module, $path,
        'should be a Number'
      );
    }

    // Should the number have a specific unit?
    $value-unit: map-get($schema, 'unit');
    @if $value-unit != null {
      // A single string may be specified for brevity.
      @if type-of($value-unit) == 'string' {
        @if k-unit-or-none($value) != $value-unit {
          @return k-unit-error($module, $path, $value-unit);
        }
      }
      @elseif type-of($value-unit) == 'list' {
        @if index($value-unit, k-unit-or-none($value)) == null {
          @return k-unit-error($module, $path, $value-unit);
        }
      }
    }

    @return k-validate-enum($value, $schema, $module, $path);
  }
  @elseif $expected-type == 'string' {
    @if $value-type != 'string' {
      @return k-schema-error(
        $module, $path,
        'should be a String'
      );
    }

    @return k-validate-enum($value, $schema, $module, $path);
  }
  @elseif $expected-type == 'color' {
    @if $value-type != 'color' {
      @return k-schema-error(
        $module, $path,
        'should be a Color'
      );
    }

    @return k-validate-enum($value, $schema, $module, $path);
  }
  @elseif $expected-type == 'boolean' {
    @if $value-type != 'boolean' {
      @return k-schema-error(
        $module, $path,
        'should be a Boolean'
      );
    }

    @return k-validate-enum($value, $schema, $module, $path);
  }
  @elseif $expected-type == 'list' {
    @if $value-type != 'list' {
      @return k-schema-error(
        $module, $path,
        'should be a List'
      );
    }

    // Is there a schema defined for list items?
    $item-schema: map-get($schema, 'items');
    @if $item-schema {
      @for $i from 1 through length($value) {
        $p-value: nth($value, $i);
        $p-path: "#{$path}[#{$i}]";

        @if not k-validate($p-value, $item-schema, $module, $p-path) {
          @return false;
        }
      }
    }

    @return true;
  }
  @elseif $expected-type == 'map' {
    @if $value-type != 'map' {
      @return k-schema-error(
        $module, $path,
        'should be a Map'
      );
    }

    // Do we have specific schemas for certain properties?
    $map-properties: map-get($schema, 'properties');
    @if $map-properties != null {
      @each $p-name, $p-schema in $map-properties {
        $p-value: map-get($value, $p-name);
        $p-path: "#{$path}.#{$p-name}";

        @if $p-value != null {
          @if not k-validate($p-value, $p-schema, $module, $p-path) {
            @return false;
          }
        }
      }
    }

    // Are there any required properties?
    $required-properties: map-get($schema, 'required');
    @if $required-properties != null {
      @each $p-name in $required-properties {
        $p-value: map-get($value, $p-name);
        $p-path: "#{$path}.#{$p-name}";

        @if $p-value == null {
          @return k-schema-error(
            $module, $p-path,
            'is required'
          );
        }
      }
    }

    // Are there any additional properties? Additional properties are not listed
    // exhaustively, and must all validate against a single schema.
    $additional-properties: map-get($schema, 'additional-properties');
    @if $additional-properties != null {
      @each $p-name, $p-value in $value {
        // Do not validate the property if it is already a regular property.
        @if not map-has-key(k-one-of($map-properties, ()), $p-name) {
          $p-path: "#{$path}.#{$p-name}";

          @if not k-validate($p-value, $additional-properties, $module, $p-path) {
            @return false;
          }
        }
      }
    }

    @return true;
  }

  // If we end up here, something went wrong.
  // Assume the result should be false.
  @return k-schema-error(
    $module, $path,
    'is invalid'
  );
}

/// Validate if a value belongs to an enumeration.
///
/// @group schema
///
/// @parameter {Any}    $value  - any value
/// @parameter {Map}    $schema - a Sass schema with an enumeration clause
/// @parameter {String} $module - name of caller module, for error reporting
/// @parameter {String} $path   - validated variable name, for error reporting
///
/// @return {Boolean}
@function k-validate-enum($value, $schema, $module, $path) {
  $expected-values: map-get($schema, 'enum');

  @if $expected-values != null {
    @if index($expected-values, $value) == null {
      @return k-schema-error(
        $module, $path,
        "should be a one of #{$expected-values}"
      );
    }
  }

  @return true;
}

/// Get the value or a unit, or return none if not defined.
///
/// @group schema
///
/// @parameter {Any} $value - a value
///
/// @return {String}
@function k-unit-or-none($value) {
  $unit: unit($value);

  @if $unit == '' {
    @return none;
  }
  @else {
    @return unquote($unit);
  }
}
