//
// Copyright (c) Antmicro
//
// Full license details are defined in the 'LICENSE' file.
//
#define REMOVE_DUMMY_ROWS
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using TermSharp.Misc;
using TermSharp.Rows;
using Xwt;
using Xwt.Drawing;

namespace TermSharp
{
    public class Terminal : HBox
    {
        public Terminal(Func<bool> focusProvider = null)
        {
            if(focusProvider == null)
            {
                this.focusProvider = () => canvas.HasFocus;
            } 
            else
            {
                this.focusProvider = () => focusProvider() && canvas.HasFocus;
            }

            rows = new List<IRow>();
            heightMap = new List<double>();
            layoutParameters = new LayoutParameters(Font, DefaultGray, Colors.Black, Colors.LightSlateGray);

            layoutParameters.Font = Font.SystemMonospaceFont;
            canvas = new TerminalCanvas(this);
            cursor = new Cursor(this, canvas);
            PackStart(canvas, true, true);
            scrollbar = new VScrollbar();
            scrollbar.Sensitive = false;

            PackEnd(scrollbar);

            canvas.MouseScrolled += OnCanvasMouseScroll;
            canvas.BoundsChanged += OnCanvasBoundsChanged;
            canvas.ButtonPressed += OnCanvasButtonPressed;
            canvas.ButtonReleased += OnCanvasButtonReleased;
            canvas.MouseMoved += OnCanvasMouseMoved;

            scrollbar.ValueChanged += OnScrollbarValueChanged;
            autoscrollEnabled = new TaskCompletionSource<bool>();
            HandleAutoscrollAsync();
            canvas.CanGetFocus = true;
            scrollbar.StepIncrement = Utilities.GetLineSizeFromLayoutParams(layoutParameters).Height;
        }

        public void Close()
        {
            scrollbar.ValueChanged -= OnScrollbarValueChanged;
            canvas.MouseScrolled -= OnCanvasMouseScroll;
            canvas.BoundsChanged -= OnCanvasBoundsChanged;
            canvas.ButtonPressed -= OnCanvasButtonPressed;
            canvas.ButtonReleased -= OnCanvasButtonReleased;
            canvas.MouseMoved -= OnCanvasMouseMoved;
            cursor.Dispose();
            base.Dispose(disposing: true);
        }

        public void AppendRow(IRow row, bool treatRowAsNonDummy = false)
        {
            var weWereAtEnd = scrollbar.Value == GetMaximumScrollbarValue();

            var heightMapNeedsRebuilding = false;
            if(treatRowAsNonDummy)
            {
                var position = GetScreenRowIndex(Cursor.Position.Y);
                var rowId = Math.Min(position, rows.Count - 1);

                if(rowId < rows.Count - 1)
                {
                    rows.RemoveRange(rowId + 1, rows.Count - rowId - 1);
                    heightMapNeedsRebuilding = true;
                }

                // this counts in the row we are about to add
                lastNonDummyRow = rows.Count;
            }

            rows.Add(row);
            row.PrepareForDrawing(layoutParameters);

            if(heightMapNeedsRebuilding)
            {
                // this must be done after adding the row, otherwise the cursor will be wrongly placed
                RebuildHeightMap(true);
            }
            else
            {
                AddToHeightMap(row.PrepareForDrawing(layoutParameters));
            }
            RefreshInner(weWereAtEnd);
        }

        public new void Clear()
        {
            Cursor.Position = new IntegerPosition();
            rows.Clear();
            RebuildHeightMap(true);
        }

        public void Refresh()
        {
            var weWereAtEnd = scrollbar.Value == GetMaximumScrollbarValue();
            RebuildHeightMap(true);
            RefreshInner(weWereAtEnd);
        }

        public void Redraw()
        {
            canvas.Redraw();
        }

        public void PageUp()
        {
            SetScrollbarValue(scrollbar.Value - scrollbar.PageSize);
        }

        public void PageDown()
        {
            SetScrollbarValue(scrollbar.Value + scrollbar.PageSize);
        }

        public void LineUp()
        {
            ScrollRows(-1);
        }

        public void LineDown()
        {
            ScrollRows(1);
        }

        public void ClearSelection()
        {
            canvas.SelectedArea = default(Rectangle);
            currentScrollStart = null;
            RefreshSelection();
        }

        public ClipboardData CollectClipboardData()
        {
            var result = new ClipboardData();
            foreach(var row in rows)
            {
                row.FillClipboardData(result);
            }
            return result;
        }

        public IRow GetScreenRow(int screenPosition, bool treatRowAsNonDummy = false)
        {
            var position = GetScreenRowIndex(screenPosition);
            if(treatRowAsNonDummy)
            {
                lastNonDummyRow = Math.Max(lastNonDummyRow, position);
            }
            return rows[Math.Min(position, rows.Count - 1)];
        }

        public IRow GetFirstScreenRow(out double hiddenHeight)
        {
            double rowStart;
            var result = rows[FindRowIndexAtPosition(GetMaximumScrollbarValue(), out rowStart)];
            hiddenHeight = GetMaximumScrollbarValue() - rowStart;
            return result;
        }

        public void EraseScreen(IntegerPosition from, IntegerPosition to, Color? background)
        {
            for(var rowScreenPosition = from.Y; rowScreenPosition <= to.Y; rowScreenPosition++)
            {
                var row = GetScreenRow(rowScreenPosition);
                row.Erase(rowScreenPosition == from.Y ? from.X : 0, rowScreenPosition == to.Y ? to.X : row.CurrentMaximalCursorPosition, background);
            }
            canvas.Redraw();
        }

        public void MoveScrollbarToBeginning()
        {
            SetScrollbarValue(scrollbar.LowerValue);
        }

        public void MoveScrollbarToEnd()
        {
            SetScrollbarValue(scrollbar.UpperValue - canvas.CachedBounds.Height);
        }

        public Font CurrentFont
        {
            get
            {
                return layoutParameters.Font;
            }
            set
            {
                bool fontSizeDecreased = layoutParameters.Font.Size > value.Size;
                layoutParameters.Font = value;
                Redraw();
                if(rows.Count > 0)
                {
                    if(fontSizeDecreased)
                    {
                        RebuildHeightMap();
                    }
                    OnCanvasBoundsChanged(null, null);
                }
            }
        }

        public int RowCount
        {
            get
            {
                return rows.Count;
            }
        }

        public int ScreenRowCount
        {
            get
            {
                double unused;
                var firstRowNo = FindRowIndexAtPosition(GetMaximumScrollbarValue(), out unused);
                return RowCount - firstRowNo;
            }
        }

        public double ScreenSize
        {
            get
            {
                return canvas.CachedBounds.Height;
            }
        }

        public SelectionMode SelectionMode
        {
            get
            {
                return canvas.SelectionMode;
            }
            set
            {
                canvas.SelectionMode = value;
            }
        }

        public new Cursor Cursor
        {
            get
            {
                return cursor;
            }
        }

        public Color DefaultForeground
        {
            get
            {
                return layoutParameters.DefaultForeground;
            }
            set
            {
                layoutParameters.DefaultForeground = value;
            }
        }

        public Color DefaultBackground
        {
            get
            {
                return layoutParameters.DefaultBackground;
            }
            set
            {
                layoutParameters.DefaultBackground = value;
            }
        }

        public Color SelectionColor
        {
            get
            {
                return layoutParameters.SelectionColor;
            }
            set
            {
                layoutParameters.SelectionColor = value;
            }
        }

        public double InnerMarginLeft
        {
            get
            {
                return canvas.MarginLeft;
            }
            set
            {
                canvas.MarginLeft = value;
            }
        }

        public double InnerMarginRight
        {
            get
            {
                return canvas.MarginRight;
            }
            set
            {
                canvas.MarginRight = value;
            }
        }

        public double InnerMarginBottom
        {
            get
            {
                return canvas.MarginBottom;
            }
            set
            {
                canvas.MarginBottom = value;
            }
        }

        public double InnerMarginTop
        {
            get
            {
                return canvas.MarginTop;
            }
            set
            {
                canvas.MarginTop = value;
            }
        }

        public WidgetSpacing InnerMargin
        {
            get
            {
                return canvas.Margin;
            }
            set
            {
                canvas.Margin = value;
            }
        }

        public Menu ContextMenu { get; set; }

        public new event EventHandler<KeyEventArgs> KeyPressed
        {
            add
            {
                canvas.KeyPressed += value;
            }
            remove
            {
                canvas.KeyPressed -= value;
            }
        }

        public event Action Initialized;

        static internal Color DefaultGray = new Color(0.75, 0.75, 0.75);

        private void OnScrollbarValueChanged(object sender, EventArgs e)
        {
            ScrollbarValueChanged();
        }

        private void ScrollbarValueChanged()
        {
            double rowOffset;
            canvas.FirstRowToDisplay = FindRowIndexAtPosition(scrollbar.Value, out rowOffset);
            canvas.FirstRowHeight = rowOffset;
            canvas.OffsetFromFirstRow = scrollbar.Value - rowOffset;
            RefreshSelection();
        }

        private void OnCanvasButtonPressed(object sender, ButtonEventArgs e)
        {
            if(e.Button == PointerButton.Left)
            {
                canvas.SetFocus();

                var position = e.Position;
                lastMousePosition = position;
                position.Y += scrollbar.Value;
                currentScrollStart = position;
                foreach(var row in rows)
                {
                    row.ResetSelection();
                }
                HandleMultiplePresses(e);
                RefreshSelection();
            }
            if(e.Button == PointerButton.Right)
            {
                var contextMenu = ContextMenu;
                if(contextMenu != null)
                {
                    contextMenu.Popup();
                }
            }
        }

        private Rectangle GetWordRect(int x, int y)
        {
            var row = rows[FindRowIndexAtPosition(y, out var yStart)];
            var rowText = row.TextContent;
            var colIdx = row.GetColumnIndex(x);

            var wrappedRow = (row.LineHeight == 0.0) ? 0 : (int)((y - yStart) / row.LineHeight);

            var selectedWordStart = colIdx + (row.MaximalColumn + 1) * wrappedRow;
            while(selectedWordStart > 0 && selectedWordStart < rowText.Length)
            {
                if(char.IsWhiteSpace(rowText[selectedWordStart - 1]))
                {
                    break;
                }
                selectedWordStart--;
            }
            var selectedWordEnd = colIdx + (row.MaximalColumn + 1) * wrappedRow;
            while(selectedWordEnd > 0 && selectedWordEnd < rowText.Length - 1)
            {
                if(char.IsWhiteSpace(rowText[selectedWordEnd + 1]))
                {
                    break;
                }
                selectedWordEnd++;
            }

            var startX = row.IndexToColumn(selectedWordStart % (row.MaximalColumn + 1));
            var endX = row.IndexToColumn(selectedWordEnd % (row.MaximalColumn + 1));

            var startWrapRow = (int)(selectedWordStart / (row.MaximalColumn + 1));
            var endWrapRow = (int)(selectedWordEnd / (row.MaximalColumn + 1));
            var wrapRowDiff = endWrapRow - startWrapRow;
            var width = endX - startX;

            // The required y range is exclusive so we need it to be bigger by an epsilon, since the +0.001
            var rectY = yStart + 0.001 + (startWrapRow*row.LineHeight);
            return new Rectangle(startX, rectY, width, wrapRowDiff * row.LineHeight);
        }

        private Rectangle GetLineRect(int x, int y)
        {
            var row = rows[FindRowIndexAtPosition(y, out var yStart)];
            var rowText = row.TextContent;

            // See comment in GetWordRect
            return new Rectangle(0, yStart + 0.001, row.IndexToColumn(rowText.Length), (row.SublineCount - 1) * row.LineHeight);
        }

        private void HandleMultiplePresses(ButtonEventArgs e)
        {
            switch(e.MultiplePress)
            {
            case 1:
                SelectionMode = SelectionMode.Normal;
                break;
            case 2:
                SelectionMode = SelectionMode.Word;
                break;
            case 3:
                SelectionMode = SelectionMode.Line;
                break;
            }
        }

        private void OnCanvasButtonReleased(object sender, ButtonEventArgs e)
        {
            if(e.Button == PointerButton.Left)
            {
                SetAutoscrollValue(0);
                var mousePosition = e.Position;
                mousePosition.Y += scrollbar.Value;
                if(mousePosition == (currentScrollStart ?? default(Point)) && SelectionMode == SelectionMode.Normal)
                {
                    canvas.SelectedArea = default(Rectangle);
                }
                currentScrollStart = null;

                RefreshSelection();

                if(canvas.SelectedArea != default(Rectangle))
                {
                    Clipboard.SetPrimaryText(CollectClipboardData().Text);
                }
            }
        }

        private void OnCanvasMouseMoved(object sender, MouseMovedEventArgs e)
        {
            if(!currentScrollStart.HasValue)
            {
                return;
            }
            lastMousePosition = e.Position;
            if(e.Position.Y < 0)
            {
                SetAutoscrollValue((int)e.Position.Y);
            }
            else if(e.Position.Y > canvas.CachedBounds.Height)
            {
                SetAutoscrollValue((int)(e.Position.Y - canvas.CachedBounds.Height));
            }
            else
            {
                SetAutoscrollValue(0);
            }
            RefreshSelection();
        }


        private void OnCanvasMouseScroll(object sender, MouseScrolledEventArgs e)
        {
            int modifier;
            switch(e.Direction)
            {
            case ScrollDirection.Up:
                modifier = -1;
                break;
            case ScrollDirection.Down:
                modifier = 1;
                break;
            default:
                modifier = 0;
                break;
            }
            SetScrollbarValue(scrollbar.Value + scrollbar.StepIncrement * modifier);
        }

#if DEBUG
        private async void OnCanvasBoundsChanged(object sender, EventArgs e)
#else
        private void OnCanvasBoundsChanged(object sender, EventArgs e)
#endif
        {
            canvas.SelectedArea = default(Rectangle);
            // refresh cached bounds value
            canvas.CachedBounds = canvas.Bounds;

            double oldPosition;
            var firstDisplayedRowIndex = FindRowIndexAtPosition(scrollbar.Value, out oldPosition);
            var oldScrollbarValue = scrollbar.Value;
            var weWereAtEnd = scrollbar.Value == GetMaximumScrollbarValue();

            layoutParameters.Width = canvas.Size.Width;

            #if REMOVE_DUMMY_ROWS
            var numberOfVisibleLines = (int)Math.Floor(ScreenSize / rows[0].LineHeight);
            if(heightMap.Count > lastNonDummyRow + 1 && lastNonDummyRow < numberOfVisibleLines)
            {
                rows.RemoveRange(lastNonDummyRow + 1, heightMap.Count - lastNonDummyRow - 2);
            }
            #endif
#if DEBUG
            if(!RebuildHeightMap(false))
            {
                var boundChangedGeneration = ++canvasBoundChangedGeneration;

                await Task.Delay(TimeSpan.FromMilliseconds(200));
                if(boundChangedGeneration != canvasBoundChangedGeneration)
                {
                    return;
                }
                RebuildHeightMap(true);
            }
#else
            RebuildHeightMap(true);
#endif
            scrollbar.UpperValue = GetMaximumHeight();

            if(!weWereAtEnd)
            {
                // difference between old and new position of the first displayed row:
                var diff = GetPositionOfTheRow(firstDisplayedRowIndex) - oldPosition;
                SetScrollbarValue(oldScrollbarValue + diff);
            }
            else
            {
                MoveScrollbarToEnd();
            }
            canvas.Redraw();
            if(!isInitialized)
            {
                isInitialized = true;
                CallInitializedEvent();
            }
        }

        private void CallInitializedEvent()
        {
            var initialized = Initialized;
            if(initialized != null)
            {
                initialized();
            }
        }

        private void RefreshInner(bool weWereAtEnd)
        {
            canvas.Redraw();
            scrollbar.Sensitive = GetMaximumHeight() > canvas.CachedBounds.Height;

            scrollbar.UpperValue = GetMaximumHeight();
            if(weWereAtEnd)
            {
                MoveScrollbarToEnd();
            }
        }

        private void SetAutoscrollValue(int value)
        {
            autoscrollStep = value;
            if(value != 0)
            {
                autoscrollEnabled.TrySetResult(true);
            }
            else
            {
                if(autoscrollEnabled.Task.IsCompleted)
                {
                    autoscrollEnabled = new TaskCompletionSource<bool>();
                }
            }
        }

        private void SetScrollbarValue(double value)
        {
            var finalValue = Math.Max(0, value);
            finalValue = Math.Min(finalValue, GetMaximumScrollbarValue());
            scrollbar.Value = finalValue;
        }

        // This is not the same as `Rectangle.Union`. It's possible for the width to be
        // a negative value and it's intentional because later we use it to derive the
        // 'selection direction' in `MonospaceTextRow`. This is for handling single lines with
        // multiple line breaks. There are two possible cases (not 4 because of the swapping):
        //
        // 1) First one is the 'normal one' when a.X < b.X, where we get a union
        //  a
        //  +---+-----+
        //  |   |     |
        //  +---+ b   |
        //  |     +---+
        //  |     |   |
        //  +-----+---+
        //
        // 2) Second is the 'negative width one' when a.X > b.X, where we go
        // from a's top left to b's bottom right
        //          a
        //     +----+---+
        //     |    |   |
        // b   |    +---+
        // +---+    |
        // |   |    |
        // +---+----+
        //
        private Rectangle CombineTwoRects(Rectangle a, Rectangle b)
        {
            if(a.Y > b.Y || (a.Y == b.Y && a.X > b.X) || (a.X == b.X && a.Y == b.Y && a.Width > b.Width))
            {
                Utilities.Swap(ref a, ref b);
            }

            return new Rectangle(a.X, a.Y, b.X - a.X + b.Width, b.Y - a.Y + b.Height);
        }

        private bool ShouldNotHighlight(Rectangle selectedArea, SelectionMode selectionMode)
        {
            if(selectionMode != SelectionMode.Normal)
            {
                return false;
            }

            const int boxSize = 3;
            return Math.Abs(canvas.SelectedArea.Width) < boxSize && Math.Abs(canvas.SelectedArea.Height) < boxSize;
        }

        private void RefreshSelection()
        {
            if(currentScrollStart.HasValue)
            {
                var scrollStart = currentScrollStart.Value;
                if(SelectionMode == SelectionMode.Normal)
                {
                    canvas.SelectedArea = new Rectangle(scrollStart.X, scrollStart.Y, lastMousePosition.X - scrollStart.X, lastMousePosition.Y + scrollbar.Value - scrollStart.Y);
                }
                else if(SelectionMode == SelectionMode.Word)
                {
                    var startRect = GetWordRect((int)scrollStart.X, (int)scrollStart.Y);
                    var endRect = GetWordRect((int)lastMousePosition.X, (int)(lastMousePosition.Y + scrollbar.Value));
                    canvas.SelectedArea = CombineTwoRects(startRect, endRect);
                }
                else if(SelectionMode == SelectionMode.Line)
                {
                    Rectangle startRect = GetLineRect((int)scrollStart.X, (int)scrollStart.Y);
                    Rectangle endRect = GetLineRect((int)lastMousePosition.X, (int)(lastMousePosition.Y + scrollbar.Value));
                    canvas.SelectedArea = CombineTwoRects(startRect, endRect);
                }

                if(ShouldNotHighlight(canvas.SelectedArea, SelectionMode))
                {
                    canvas.SelectedArea = default(Rectangle);
                }
            }
            canvas.Redraw();
        }

        private async void HandleAutoscrollAsync()
        {
            while(true)
            {
                await Task.Delay(TimeSpan.FromMilliseconds(40));
                if(autoscrollStep != 0)
                {
                    if(Math.Abs(autoscrollStep) > scrollbar.PageSize / 2)
                    {
                        autoscrollStep = (int)(Math.Sign(autoscrollStep) * scrollbar.PageSize / 2);
                    }
                    SetScrollbarValue(scrollbar.Value + autoscrollStep);
                }
                await autoscrollEnabled.Task;
            }
        }

        private bool RebuildHeightMap(bool continueEvenIfLongTask = true)
        {
            if(rows.Count == 0)
            {
                heightMap = new List<double>();
                return true;
            }

            var oldFirstScreenRow = GetScreenRowIndex(0);

            var stopwatch = new Stopwatch();
            stopwatch.Start();
            List<double> newHeightMap;
            if(unfinishedHeightMap != null && unfinishedHeightMap.Count == rows.Count)
            {
                newHeightMap = unfinishedHeightMap;
            }
            else
            {
                newHeightMap = new List<double>(rows.Count);
                unfinishedHeightMap = newHeightMap;
            }
            var heightSoFar = 0.0;
            for(var i = 0; i < rows.Count; i++)
            {
                heightSoFar += rows[i].PrepareForDrawing(layoutParameters);
                newHeightMap.Add(heightSoFar);
                if(!continueEvenIfLongTask && (i % HeightMapCheckTimeoutEveryNthRow) == 1 && stopwatch.Elapsed > HeightMapRebuildTimeout)
                {
                    return false;
                }
            }
            heightMap = newHeightMap;
            unfinishedHeightMap = null;

            scrollbar.PageSize = canvas.CachedBounds.Height; // we update it here to get new value on GetScreenRowId (it depends on height map and this value)
            var firstScreenRow = GetScreenRowIndex(0);
            var diff = firstScreenRow - oldFirstScreenRow;
            cursor.Position = cursor.Position.ShiftedByY(-diff);

            return true;
        }

        private void AddToHeightMap(double value)
        {
            if(heightMap.Count == 0)
            {
                heightMap.Add(value);
                return;
            }
            heightMap.Add(value + heightMap[heightMap.Count - 1]);
        }

        private double GetPositionOfTheRow(int rowIndex)
        {
            return rowIndex > 0 ? heightMap[rowIndex - 1] : 0.0;
        }

        private double GetMaximumScrollbarValue()
        {
            return Math.Max(0, GetMaximumHeight() - scrollbar.PageSize);
        }

        private double GetMaximumHeight()
        {
            if(heightMap.Count == 0)
            {
                return 0;
            }
            return heightMap[heightMap.Count - 1];
        }

        private int FindRowIndexAtPosition(double position, out double rowStart)
        {
            var result = heightMap.BinarySearch(position);
            if(result < 0)
            {
                result = ~result;
            }
            else
            {
                result++; // because heightMap[i] shows where ith row *ends* and therefore where (i+1)th starts
            }
            if(result == heightMap.Count)
            {
                result--;
            }
            rowStart = GetPositionOfTheRow(result);
            return result;
        }

        private int GetScreenRowIndex(int screenPosition)
        {
            double unused;
            return FindRowIndexAtPosition(GetMaximumScrollbarValue(), out unused) + screenPosition;
        }

        private void ScrollRows(int offset)
        {
            var firstDisplayedRowIndex = FindRowIndexAtPosition(scrollbar.Value, out _);
            var newPosition = GetPositionOfTheRow(firstDisplayedRowIndex + offset);
            SetScrollbarValue(newPosition);
        }

        private List<double> heightMap;
        private List<double> unfinishedHeightMap;
#if DEBUG
        private int canvasBoundChangedGeneration;
#endif
        private Point? currentScrollStart;
        private Point lastMousePosition;
        private int autoscrollStep;
        private TaskCompletionSource<bool> autoscrollEnabled;
        private int lastNonDummyRow;
        private bool isInitialized;

        private readonly List<IRow> rows;
        private readonly LayoutParameters layoutParameters;
        private readonly VScrollbar scrollbar;
        private readonly TerminalCanvas canvas;
        private readonly Cursor cursor;
        private readonly Func<bool> focusProvider;

        private static readonly TimeSpan HeightMapRebuildTimeout = TimeSpan.FromMilliseconds(30);
        private const int HeightMapCheckTimeoutEveryNthRow = 1000;

        internal sealed class TerminalCanvas : Canvas
        {
            public TerminalCanvas(Terminal parent)
            {
                this.parent = parent;
                Cursor = CursorType.IBeam;
            }

            public void Redraw()
            {
                if(drawn)
                {
                    QueueDraw();
                    drawn = false;
                }
            }

            public Rectangle CachedBounds
            {
                get
                {
                    if(!cachedBounds.HasValue)
                    {
                        cachedBounds = Bounds;
                    }
                    return cachedBounds.Value;
                }
                set
                {
                    cachedBounds = value;
                }
            }
            public int FirstRowToDisplay { get; set; }

            public double FirstRowHeight { get; set; }

            public double OffsetFromFirstRow { get; set; }

            public Rectangle SelectedArea { get; set; }

            public SelectionMode SelectionMode { get; set; }

            protected override void OnDraw(Context ctx, Rectangle dirtyRect)
            {
                var screenSelectedArea = SelectedArea;
                var selectionDirection = SelectionDirection.SE;
                if(screenSelectedArea.Width < 0)
                {
                    selectionDirection = (SelectionDirection)((int)selectionDirection + 1);
                    screenSelectedArea.X += screenSelectedArea.Width;
                    screenSelectedArea.Width = -screenSelectedArea.Width;
                }
                if(screenSelectedArea.Height < 0)
                {
                    selectionDirection = (SelectionDirection)((int)selectionDirection + 2);
                    screenSelectedArea.Y += screenSelectedArea.Height;
                    screenSelectedArea.Height = -screenSelectedArea.Height;
                }
                screenSelectedArea.Y -= FirstRowHeight;

                var heightSoFar = 0.0;

                ctx.Save();
                ctx.SetColor(parent.DefaultBackground);
                ctx.Rectangle(new Rectangle(0, 0, CachedBounds.Width, CachedBounds.Height));
                ctx.Fill();
                ctx.Restore();

                ctx.Translate(0, -OffsetFromFirstRow);
                ctx.Save();
                var i = FirstRowToDisplay;
                var cursorRow = parent.GetScreenRowIndex(parent.Cursor.Position.Y);
                while(i < parent.rows.Count && heightSoFar - OffsetFromFirstRow < CachedBounds.Height)
                {
                    var height = parent.rows[i].PrepareForDrawing(parent.layoutParameters);
                    var rowRectangle = new Rectangle(0, heightSoFar, parent.layoutParameters.Width, height);
                    var selectedAreaInRow = rowRectangle.Intersect(screenSelectedArea);
                    if(SelectionMode != SelectionMode.Block && selectedAreaInRow != default(Rectangle) &&
                       (screenSelectedArea.Y <= rowRectangle.Y || screenSelectedArea.Y + screenSelectedArea.Height >= rowRectangle.Y + rowRectangle.Height))
                    {
                        if(rowRectangle.Y < screenSelectedArea.Y)
                        {
                            // I'm the first row (and there is a second row)
                            selectedAreaInRow.X = SelectedArea.Height > 0 ? SelectedArea.X : SelectedArea.X + SelectedArea.Width;
                            selectedAreaInRow.Width = parent.layoutParameters.Width - selectedAreaInRow.X;
                        }
                        else if(rowRectangle.Y + rowRectangle.Height > screenSelectedArea.Y + screenSelectedArea.Height)
                        {
                            // I'm the last row (and there is some other row)
                            selectedAreaInRow.Width = SelectedArea.Height > 0 ? SelectedArea.X + SelectedArea.Width : SelectedArea.X;
                            selectedAreaInRow.X = 0;
                        }
                        else
                        {
                            // nor the first neither the last one - must be one of the middle rows
                            selectedAreaInRow.X = 0;
                            selectedAreaInRow.Width = parent.layoutParameters.Width;
                        }
                    }
                    if(selectedAreaInRow != default(Rectangle))
                    {
                        selectedAreaInRow.Y -= heightSoFar;
                    }

                    ctx.Save();
                    var hasFocus = parent.focusProvider();
                    parent.rows[i].Draw(ctx, selectedAreaInRow, selectionDirection, parent.SelectionMode);
                    if(parent.Cursor.Enabled && i == cursorRow && (parent.Cursor.BlinkState || !hasFocus))
                    {
                        parent.rows[i].DrawCursor(ctx, parent.Cursor.Position.X, hasFocus);
                    }
                    ctx.Restore();

                    heightSoFar += height;
                    ctx.Translate(0, height);
                    i++;
                }
                ctx.Restore();
                drawn = true;
            }

            private bool drawn;
            private Rectangle? cachedBounds;
            private readonly Terminal parent;
        }
    }
}
