// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// ignore_for_file: camel_case_types, non_constant_identifier_names

import 'dart:ffi' as ffi;
import 'dart:io';
import 'dart:typed_data';

/// Determines the winding rule that decides how the interior of a Path is
/// calculated.
///
/// This enum is used by the [Path] constructor
// must match ordering in //third_party/skia/include/core/SkPathTypes.h
enum FillType {
  /// The interior is defined by a non-zero sum of signed edge crossings.
  nonZero,

  /// The interior is defined by an odd number of edge crossings.
  evenOdd,
}

/// A set of operations applied to two paths.
// Sync with //third_party/skia/include/pathops/SkPathOps.h
enum PathOp {
  /// Subtracts the second path from the first.
  difference,

  /// Creates a new path representing the intersection of the first and second.
  intersect,

  /// Creates a new path representing the union of the first and second
  /// (includive-or).
  union,

  /// Creates a new path representing the exclusive-or of two paths.
  xor,

  /// Creates a new path that subtracts the first path from the second.s
  reversedDifference,
}

/// The commands used in a [Path] object.
///
/// This enumeration is a subset of the commands that SkPath supports.
// Sync with //third_party/skia/include/core/SkPathTypes.h
enum PathVerb {
  /// Picks up the pen and moves it without drawing. Uses two point values.
  moveTo,

  /// A straight line from the current point to the specified point.
  lineTo,

  _quadTo,
  _conicTo,

  /// A cubic bezier curve from the current point.
  ///
  /// The next two points are used as the first control point. The next two
  /// points form the second control point. The next two points form the
  /// target point.
  cubicTo,

  /// A straight line from the current point to the last [moveTo] point.
  close,
}

/// A proxy class for [Path.replay].
///
/// Allows implementations to easily inspect the contents of a [Path].
abstract class PathProxy {
  /// Picks up the pen and moves to absolute coordinates x,y.
  void moveTo(double x, double y);

  /// Draws a straight line from the current point to absolute coordinates x,y.
  void lineTo(double x, double y);

  /// Creates a cubic Bezier curve from the current point to point x3,y3 using
  /// x1,y1 as the first control point and x2,y2 as the second.
  void cubicTo(
      double x1, double y1, double x2, double y2, double x3, double y3);

  /// Draws a straight line from the current point to the last [moveTo] point.
  void close();

  /// Called by [Path.replay] to indicate that a new path is being played.
  void reset() {}
}

/// A path proxy that can print the SVG path-data representation of this path.
class SvgPathProxy implements PathProxy {
  final StringBuffer _buffer = StringBuffer();

  @override
  void reset() {
    _buffer.clear();
  }

  @override
  void close() {
    _buffer.write('Z');
  }

  @override
  void cubicTo(
      double x1, double y1, double x2, double y2, double x3, double y3) {
    _buffer.write('C$x1,$y1 $x2,$y2 $x3,$y3');
  }

  @override
  void lineTo(double x, double y) {
    _buffer.write('L$x,$y');
  }

  @override
  void moveTo(double x, double y) {
    _buffer.write('M$x,$y');
  }

  @override
  String toString() => _buffer.toString();
}

/// Creates a path object to operate on.
///
/// First, build up the path contours with the [moveTo], [lineTo], [cubicTo],
/// and [close] methods. All methods expect absolute coordinates.
///
/// Finally, use the [dispose] method to clean up native resources. After
/// [dispose] has been called, this class must not be used again.
class Path implements PathProxy {
  /// Creates an empty path object with the specified fill type.
  Path([FillType fillType = FillType.nonZero])
      : _path = _createPathFn(fillType.index);

  /// Creates a copy of this path.
  factory Path.from(Path other) {
    final Path result = Path(other.fillType);
    other.replay(result);
    return result;
  }

  /// The [FillType] of this path.
  FillType get fillType {
    assert(_path != null);
    return FillType.values[_getFillTypeFn(_path!)];
  }

  ffi.Pointer<_SkPath>? _path;
  ffi.Pointer<_PathData>? _pathData;

  /// The number of points used by each [PathVerb].
  static const Map<PathVerb, int> pointsPerVerb = <PathVerb, int>{
    PathVerb.moveTo: 2,
    PathVerb.lineTo: 2,
    PathVerb.cubicTo: 6,
    PathVerb.close: 0,
  };

  /// Makes the appropriate calls using [verbs] and [points] to replay this path
  /// on [proxy].
  ///
  /// Calls [PathProxy.reset] first if [reset] is true.
  void replay(PathProxy proxy, {bool reset = true}) {
    if (reset) {
      proxy.reset();
    }
    int index = 0;
    for (final PathVerb verb in verbs.toList()) {
      switch (verb) {
        case PathVerb.moveTo:
          proxy.moveTo(points[index++], points[index++]);
        case PathVerb.lineTo:
          proxy.lineTo(points[index++], points[index++]);
        case PathVerb._quadTo:
          assert(false);
        case PathVerb._conicTo:
          assert(false);
        case PathVerb.cubicTo:
          proxy.cubicTo(
            points[index++],
            points[index++],
            points[index++],
            points[index++],
            points[index++],
            points[index++],
          );
        case PathVerb.close:
          proxy.close();
      }
    }
    assert(index == points.length);
  }

  /// The list of path verbs in this path.
  ///
  /// This may not match the verbs supplied by calls to [moveTo], [lineTo],
  /// [cubicTo], and [close] after [applyOp] is invoked.
  ///
  /// This list determines the meaning of the [points] array.
  Iterable<PathVerb> get verbs {
    _updatePathData();
    final int count = _pathData!.ref.verb_count;
    return List<PathVerb>.generate(count, (int index) {
      return PathVerb.values[_pathData!.ref.verbs.elementAt(index).value];
    }, growable: false);
  }

  /// The list of points to use with [verbs].
  ///
  /// Each verb uses a specific number of points, specified by the
  /// [pointsPerVerb] map.
  Float32List get points {
    _updatePathData();
    return _pathData!.ref.points.asTypedList(_pathData!.ref.point_count);
  }

  void _updatePathData() {
    assert(_path != null);
    _pathData ??= _dataFn(_path!);
  }

  void _resetPathData() {
    if (_pathData != null) {
      _destroyDataFn(_pathData!);
    }
    _pathData = null;
  }

  @override
  void moveTo(double x, double y) {
    assert(_path != null);
    _resetPathData();
    _moveToFn(_path!, x, y);
  }

  @override
  void lineTo(double x, double y) {
    assert(_path != null);
    _resetPathData();
    _lineToFn(_path!, x, y);
  }

  @override
  void cubicTo(
    double x1,
    double y1,
    double x2,
    double y2,
    double x3,
    double y3,
  ) {
    assert(_path != null);
    _resetPathData();
    _cubicToFn(_path!, x1, y1, x2, y2, x3, y3);
  }

  @override
  void close() {
    assert(_path != null);
    _resetPathData();
    _closeFn(_path!, true);
  }

  @override
  void reset() {
    assert(_path != null);
    _resetPathData();
    _resetFn(_path!);
  }

  /// Releases native resources.
  ///
  /// After calling dispose, this class must not be used again.
  void dispose() {
    assert(_path != null);
    _resetPathData();
    _destroyFn(_path!);
    _path = null;
  }

  /// Applies the operation described by [op] to this path using [other].
  Path applyOp(Path other, PathOp op) {
    assert(_path != null);
    assert(other._path != null);
    final Path result = Path.from(this);
    _opFn(result._path!, other._path!, op.index);
    return result;
  }
}

// TODO(dnfield): Figure out where to put this.
// https://github.com/flutter/flutter/issues/99563
final ffi.DynamicLibrary _dylib = () {
  if (Platform.isWindows) {
    return ffi.DynamicLibrary.open('path_ops.dll');
  } else if (Platform.isIOS || Platform.isMacOS) {
    return ffi.DynamicLibrary.open('libpath_ops.dylib');
  } else if (Platform.isAndroid || Platform.isLinux) {
    return ffi.DynamicLibrary.open('libpath_ops.so');
  }
  throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}');
}();

final class _SkPath extends ffi.Opaque {}

final class _PathData extends ffi.Struct {
  external ffi.Pointer<ffi.Uint8> verbs;

  @ffi.Size()
  external int verb_count;

  external ffi.Pointer<ffi.Float> points;

  @ffi.Size()
  external int point_count;
}

typedef _CreatePathType = ffi.Pointer<_SkPath> Function(int);
typedef _create_path_type = ffi.Pointer<_SkPath> Function(ffi.Int);

final _CreatePathType _createPathFn =
    _dylib.lookupFunction<_create_path_type, _CreatePathType>(
  'CreatePath',
);

typedef _MoveToType = void Function(ffi.Pointer<_SkPath>, double, double);
typedef _move_to_type = ffi.Void Function(
    ffi.Pointer<_SkPath>, ffi.Float, ffi.Float);

final _MoveToType _moveToFn = _dylib.lookupFunction<_move_to_type, _MoveToType>(
  'MoveTo',
);

typedef _LineToType = void Function(ffi.Pointer<_SkPath>, double, double);
typedef _line_to_type = ffi.Void Function(
    ffi.Pointer<_SkPath>, ffi.Float, ffi.Float);

final _LineToType _lineToFn = _dylib.lookupFunction<_line_to_type, _LineToType>(
  'LineTo',
);

typedef _CubicToType = void Function(
    ffi.Pointer<_SkPath>, double, double, double, double, double, double);
typedef _cubic_to_type = ffi.Void Function(ffi.Pointer<_SkPath>, ffi.Float,
    ffi.Float, ffi.Float, ffi.Float, ffi.Float, ffi.Float);

final _CubicToType _cubicToFn =
    _dylib.lookupFunction<_cubic_to_type, _CubicToType>('CubicTo');

typedef _CloseType = void Function(ffi.Pointer<_SkPath>, bool);
typedef _close_type = ffi.Void Function(ffi.Pointer<_SkPath>, ffi.Bool);

final _CloseType _closeFn =
    _dylib.lookupFunction<_close_type, _CloseType>('Close');

typedef _ResetType = void Function(ffi.Pointer<_SkPath>);
typedef _reset_type = ffi.Void Function(ffi.Pointer<_SkPath>);

final _ResetType _resetFn =
    _dylib.lookupFunction<_reset_type, _ResetType>('Reset');

typedef _DestroyType = void Function(ffi.Pointer<_SkPath>);
typedef _destroy_type = ffi.Void Function(ffi.Pointer<_SkPath>);

final _DestroyType _destroyFn =
    _dylib.lookupFunction<_destroy_type, _DestroyType>('DestroyPath');

typedef _OpType = void Function(
    ffi.Pointer<_SkPath>, ffi.Pointer<_SkPath>, int);
typedef _op_type = ffi.Void Function(
    ffi.Pointer<_SkPath>, ffi.Pointer<_SkPath>, ffi.Int);

final _OpType _opFn = _dylib.lookupFunction<_op_type, _OpType>('Op');

typedef _PathDataType = ffi.Pointer<_PathData> Function(ffi.Pointer<_SkPath>);
typedef _path_data_type = ffi.Pointer<_PathData> Function(ffi.Pointer<_SkPath>);

final _PathDataType _dataFn =
    _dylib.lookupFunction<_path_data_type, _PathDataType>('Data');

typedef _DestroyDataType = void Function(ffi.Pointer<_PathData>);
typedef _destroy_data_type = ffi.Void Function(ffi.Pointer<_PathData>);

final _DestroyDataType _destroyDataFn =
    _dylib.lookupFunction<_destroy_data_type, _DestroyDataType>('DestroyData');

typedef _GetFillTypeType = int Function(ffi.Pointer<_SkPath>);
typedef _get_fill_type_type = ffi.Int32 Function(ffi.Pointer<_SkPath>);

final _GetFillTypeType _getFillTypeFn =
    _dylib.lookupFunction<_get_fill_type_type, _GetFillTypeType>('GetFillType');
