// Copyright 2014 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.

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

/// Slots used for the children of [Diagonal] and [RenderDiagonal].
enum DiagonalSlot {
  topLeft,
  bottomRight,
}

/// A widget that demonstrates the usage of [SlottedMultiChildRenderObjectWidgetMixin]
/// by providing slots for two children that will be arranged diagonally.
class Diagonal extends RenderObjectWidget with SlottedMultiChildRenderObjectWidgetMixin<DiagonalSlot> {
  const Diagonal({
    Key? key,
    this.topLeft,
    this.bottomRight,
    this.backgroundColor,
  }) : super(key: key);

  final Widget? topLeft;
  final Widget? bottomRight;
  final Color? backgroundColor;

  @override
  Iterable<DiagonalSlot> get slots => DiagonalSlot.values;

  @override
  Widget? childForSlot(DiagonalSlot slot) {
    switch (slot) {
      case DiagonalSlot.topLeft:
        return topLeft;
      case DiagonalSlot.bottomRight:
        return bottomRight;
    }
  }

  // The [createRenderObject] and [updateRenderObject] methods configure the
  // [RenderObject] backing this widget with the configuration of the widget.
  // They do not need to do anything with the children of the widget, though.
  // The children of the widget are automatically configured on the
  // [RenderObject] by [SlottedRenderObjectElement.mount] and
  // [SlottedRenderObjectElement.update].

  @override
  SlottedContainerRenderObjectMixin<DiagonalSlot> createRenderObject(
    BuildContext context,
  ) {
    return RenderDiagonal(
      backgroundColor: backgroundColor,
    );
  }

  @override
  void updateRenderObject(
    BuildContext context,
    SlottedContainerRenderObjectMixin<DiagonalSlot> renderObject,
  ) {
    (renderObject as RenderDiagonal).backgroundColor = backgroundColor;
  }
}

/// A render object that demonstrates the usage of [SlottedContainerRenderObjectMixin]
/// by providing slots for two children that will be arranged diagonally.
class RenderDiagonal extends RenderBox with SlottedContainerRenderObjectMixin<DiagonalSlot>, DebugOverflowIndicatorMixin {
  RenderDiagonal({Color? backgroundColor}) : _backgroundColor = backgroundColor;

  // Getters and setters to configure the [RenderObject] with the configuration
  // of the [Widget]. These mostly contain boilerplate code, but depending on
  // where the configuration value is used, the setter has to call
  // [markNeedsLayout], [markNeedsPaint], or [markNeedsSemanticsUpdate].
  Color? get backgroundColor => _backgroundColor;
  Color? _backgroundColor;
  set backgroundColor(Color? value) {
    assert(value != null);
    if (_backgroundColor == value) {
      return;
    }
    _backgroundColor = value;
    markNeedsPaint();
  }

  // Getters to simplify accessing the slotted children.
  RenderBox? get _topLeft => childForSlot(DiagonalSlot.topLeft);
  RenderBox? get _bottomRight => childForSlot(DiagonalSlot.bottomRight);

  // The size this render object would have if the incoming constraints were
  // unconstrained; calculated during performLayout used during paint for an
  // assertion that checks for unintended overflow.
  late Size _childrenSize;

  // Returns children in hit test order.
  @override
  Iterable<RenderBox> get children {
    return <RenderBox>[
      if (_topLeft != null)
        _topLeft!,
      if (_bottomRight != null)
        _bottomRight!,
    ];
  }

  // LAYOUT

  @override
  void performLayout() {
    // Children are allowed to be as big as they want (= unconstrained).
    const BoxConstraints childConstraints = BoxConstraints();

    // Lay out the top left child and position it at offset zero.
    Size topLeftSize = Size.zero;
    final RenderBox? topLeft = _topLeft;
    if (topLeft != null) {
      topLeft.layout(childConstraints, parentUsesSize: true);
      _positionChild(topLeft, Offset.zero);
      topLeftSize = topLeft.size;
    }

    // Lay out the bottom right child and position it at the bottom right corner
    // of the top left child.
    Size bottomRightSize = Size.zero;
    final RenderBox? bottomRight = _bottomRight;
    if (bottomRight != null) {
      bottomRight.layout(childConstraints, parentUsesSize: true);
      _positionChild(
        bottomRight,
        Offset(topLeftSize.width, topLeftSize.height),
      );
      bottomRightSize = bottomRight.size;
    }

    // Calculate the overall size and constrain it to the given constraints.
    // Any overflow is marked (in debug mode) during paint.
    _childrenSize = Size(
      topLeftSize.width + bottomRightSize.width,
      topLeftSize.height + bottomRightSize.height,
    );
    size = constraints.constrain(_childrenSize);
  }

  void _positionChild(RenderBox child, Offset offset) {
    (child.parentData! as BoxParentData).offset = offset;
  }

  // PAINT

  @override
  void paint(PaintingContext context, Offset offset) {
    // Paint the background.
    if (backgroundColor != null) {
      context.canvas.drawRect(
        offset & size,
        Paint()
          ..color = backgroundColor!,
      );
    }

    void paintChild(RenderBox child, PaintingContext context, Offset offset) {
      final BoxParentData childParentData = child.parentData! as BoxParentData;
      context.paintChild(child, childParentData.offset + offset);
    }

    // Paint the children at the offset calculated during layout.
    final RenderBox? topLeft = _topLeft;
    if (topLeft != null) {
      paintChild(topLeft, context, offset);
    }
    final RenderBox? bottomRight = _bottomRight;
    if (bottomRight != null) {
      paintChild(bottomRight, context, offset);
    }

    // Paint an overflow indicator in debug mode if the children want to be
    // larger than the incoming constraints allow.
    assert(() {
      paintOverflowIndicator(
        context,
        offset,
        Offset.zero & size,
        Offset.zero & _childrenSize,
      );
      return true;
    }());
  }

  // HIT TEST

  @override
  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
    for (final RenderBox child in children) {
      final BoxParentData parentData = child.parentData! as BoxParentData;
      final bool isHit = result.addWithPaintOffset(
        offset: parentData.offset,
        position: position,
        hitTest: (BoxHitTestResult result, Offset transformed) {
          assert(transformed == position - parentData.offset);
          return child.hitTest(result, position: transformed);
        },
      );
      if (isHit) {
        return true;
      }
    }
    return false;
  }

  // INTRINSICS

  // Incoming height/width are ignored as children are always laid out unconstrained.

  @override
  double computeMinIntrinsicWidth(double height) {
    final double topLeftWidth = _topLeft?.getMinIntrinsicWidth(double.infinity) ?? 0;
    final double bottomRightWith = _bottomRight?.getMinIntrinsicWidth(double.infinity) ?? 0;
    return topLeftWidth + bottomRightWith;
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
    final double topLeftWidth = _topLeft?.getMaxIntrinsicWidth(double.infinity) ?? 0;
    final double bottomRightWith = _bottomRight?.getMaxIntrinsicWidth(double.infinity) ?? 0;
    return topLeftWidth + bottomRightWith;  }

  @override
  double computeMinIntrinsicHeight(double width) {
    final double topLeftHeight = _topLeft?.getMinIntrinsicHeight(double.infinity) ?? 0;
    final double bottomRightHeight = _bottomRight?.getMinIntrinsicHeight(double.infinity) ?? 0;
    return topLeftHeight + bottomRightHeight;
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
    final double topLeftHeight = _topLeft?.getMaxIntrinsicHeight(double.infinity) ?? 0;
    final double bottomRightHeight = _bottomRight?.getMaxIntrinsicHeight(double.infinity) ?? 0;
    return topLeftHeight + bottomRightHeight;
  }

  @override
  Size computeDryLayout(BoxConstraints constraints) {
    const BoxConstraints childConstraints = BoxConstraints();
    final Size topLeftSize = _topLeft?.computeDryLayout(childConstraints) ?? Size.zero;
    final Size bottomRightSize = _bottomRight?.computeDryLayout(childConstraints) ?? Size.zero;
    return constraints.constrain(Size(
      topLeftSize.width + bottomRightSize.width,
      topLeftSize.height + bottomRightSize.height,
    ));
  }
}

class ExampleWidget extends StatelessWidget {
  const ExampleWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Slotted RenderObject Example')),
        body: Center(
          child: Diagonal(
            topLeft: Container(
              color: Colors.green,
              height: 100,
              width: 200,
              child: const Center(
                child: Text('topLeft'),
              ),
            ),
            bottomRight: Container(
              color: Colors.yellow,
              height: 60,
              width: 30,
              child: const Center(
                child: Text('bottomRight'),
              ),
            ),
            backgroundColor: Colors.blue,
          ),
        ),
      ),
    );
  }
}

void main() {
  runApp(const ExampleWidget());
}
