//
//  TDLViewController.swift
//  ToDoList
//
//  Created by narita on 2017/09/12.
//  Copyright © 2017年 narita. All rights reserved.
//

import Cocoa

class TDLViewController: MINTLViewController, NSTableViewDelegate, NSTableViewDataSource
{

    // MARK: - class methods
    
    // MARK: - init methods
    
    // MARK: - dealloc
    
    // MARK: - NSCopying, hash, isEqual:
    
    // MARK: - NSCoding methods
    
    // MARK: - restorableState methods
    
    // MARK: - life cycle methods
    
    override func viewDidLoad()
    {
        super.viewDidLoad()
        
        // Drag & drop はアニメーション表示させる。gapにすると最後の行をDrag中に落ちる。
        self.tableView!.draggingDestinationFeedbackStyle = .sourceList
        
        self.tableView!.registerForDraggedTypes([NSPasteboard.PasteboardType.init("com.mindto01s.ToDoList.ToDoItem"),
                                                    NSPasteboard.PasteboardType.init("com.mindto01s.ToDoList.ToDoItemURL")])
        
        // NSDragOperationDeleteを追加することで、ゴミ箱が反応するようになる。
        self.tableView!.setDraggingSourceOperationMask([.link, .delete] , forLocal: false)
        self.tableView!.setDraggingSourceOperationMask([.move, .copy], forLocal: true)
    }
        
    // MARK: - action methods

    // ----------------------------
    // copy
    @IBAction func copy(_ sender : Any?)
    {        
        let theObjects = self.arrayController!.selectedObjects as! [ToDoItem]
        
        NSPasteboard.general.clearContents()
        NSPasteboard.general.writeObjects(theObjects)
    }
    
    @objc func canCopy(_ sender : Any?) -> Bool
    {
        return self.tableView?.selectedRowIndexes.isEmpty == false
    }

    // ----------------------------
    // cut
    @IBAction func cut(_ sender : Any?)
    {
        self.copy(sender)
        self.removeItem(sender)
    }
    
    @objc func canCut(_ sender : Any?) -> Bool
    {
        return self.tableView?.selectedRowIndexes.isEmpty == false
    }
    
    // ----------------------------
    // paste
    // 選択範囲の末尾に追加する。
    @IBAction func paste(_ sender : Any?)
    {
        let theContentsArray      = self.arrayController!.arrangedObjects as! [ToDoItem]
        let theSelectionIndexes   = self.arrayController!.selectionIndexes
        let theContentsArrayCount = theContentsArray.count
        let theInsertLine         = theSelectionIndexes.max()
        
        // クリップボードからToDoItemの配列を再生
        let ToDoItems: [ToDoItem] = NSPasteboard.general.pasteboardItems!.map
        {
            ToDoItem.toDoItem(fromPlist: $0, context: self.managedObjectContext!)!
        }

        if theSelectionIndexes.isEmpty || theInsertLine == theContentsArrayCount - 1
        {
            // 選択範囲が存在しない場合は、末尾に追加する
            ToDoItems.enumerated().forEach
            {
                $0.1.sortOrder = Int32(theContentsArrayCount + $0.0)
            }
        }
        else
        {
            // 選択範囲が存在する場合は、最後の選択行と次の行の間に、新しい行を追加する
            
            // 分割点から末尾までのforEachしてsortOrderをToDoItems.count分だけずらす。
            // これによって、ToDoItemsを入れる部分を開ける
            theContentsArray[Range((theInsertLine! + 1)...(theContentsArray.count - 1))].forEach
            {
                $0.sortOrder = Int32( $0.sortOrder + Int32(ToDoItems.count))
            }
            
            // その後、ToDoItemsをforEachしてsortOrderを分割点からインクリメントしながら追加する
            ToDoItems.enumerated().forEach
            {
                $0.1.sortOrder = Int32(theInsertLine! + $0.0 + 1)
            }
        }
        
        // 遅延実行して追加した行を選択する。
        DispatchQueue.main.asyncAfter(deadline: .now())
        {
            self.arrayController?.setSelectedObjects(ToDoItems)
        }
    }
    
    @objc func canPaste(_ sender : Any?) -> Bool
    {
        return NSPasteboard.general.canReadItem( withDataConformingToTypes: ["com.mindto01s.ToDoList.ToDoItem"])
    }

    // ----------------------------
    // delete
    @IBAction func delete(_ sender : Any?)
    {
        self.removeItem(sender)
    }
    
    @objc func canDelete(_ sender : Any?) -> Bool
    {
        return self.arrayController!.selectionIndexes.isEmpty == false
    }

    // ----------------------------
    @IBAction func addItem(_ sender : Any?)
    {
        let theContentsArray    = self.arrayController!.arrangedObjects as! [ToDoItem]
        
        // 選択範囲がない場合は、最後の行を選択する。
        if self.arrayController!.selectionIndexes.isEmpty
        {
            let theIndexSet: IndexSet = IndexSet([theContentsArray.count - 1])
            
            self.tableView?.selectRowIndexes(theIndexSet,
                                             byExtendingSelection: false)
        }

        // 選択範囲の最後の行の次に新たな行を挿入する
        let theSelectionIndexes = self.arrayController!.selectionIndexes
        // 1行も存在しない場合は-1を指定する
        let theLastSelectionIndex:Int = theSelectionIndexes.isEmpty ? -1 : theSelectionIndexes.max()!
        
        var theNewToDoItem: ToDoItem? = nil

        NSAnimationContext.runAnimationGroup(
        {
            context in
            
            self.tableView!.insertRows(at: IndexSet(integer: theLastSelectionIndex),
                                       withAnimation: [.effectFade, .slideDown ])
            
            theContentsArray.forEach()
            {
                if $0.sortOrder > Int32(theLastSelectionIndex)
                {
                    $0.sortOrder = $0.sortOrder + 1
                }
            }
            
            theNewToDoItem = ToDoItem.newToDoItem(context: self.managedObjectContext!)
            theNewToDoItem?.sortOrder = Int32( theLastSelectionIndex + 1)
        },
        completionHandler:
        {
            self.arrayController?.setSelectedObjects([theNewToDoItem!])
        })
    }
    
    @objc func canAddItem(_ sender : Any?) -> Bool
    {
        return self.arrayController!.canInsert
    }

    
    // ----------------------------
    @IBAction func removeItem(_ sender : Any?)
    {
        if self.arrayController!.canRemove
        {
            let theSelectedIndexes = self.arrayController!.selectionIndexes
            
            // NSAnimationContextで包み、runで現在編集中のViewをアニメーションさせ, completionでモデルを変更する。
            // モデルに従属している他のViewはアニメーションの対象でなくてもOK
            NSAnimationContext.runAnimationGroup(
                {
                    context in
                    
                    self.tableView!.removeRows(at: theSelectedIndexes, withAnimation: [.effectFade, .slideUp ])
                },
                completionHandler:
                {
                    self.arrayController!.remove(atArrangedObjectIndexes: theSelectedIndexes)
                    self.renumberingSortOrder()
                }
            )

        }
    }
    
    @objc func canRemoveItem(_ sender : Any?) -> Bool
    {
        return self.arrayController!.canRemove
    }

    // ----------------------------
    @IBAction func newPane(_ sender : Any?)
    {
        if let theVC = self.parent as! MINTLDividerViewController?
        {
            theVC.insertNewPane(after:self)
        }        
    }
    
    @objc func canNewPane(_ sender : Any?) -> Bool
    {
        return true
    }

    // ----------------------------
    @IBAction func deletePane(_ sender : Any?)
    {
        if let theVC = self.parent as! MINTLDividerViewController?
        {
            theVC.removePane(self)
        }
    }
    
    @objc func canDeletePane(_ sender : Any?) -> Bool
    {
        if let theVC = self.parent as! MINTLDividerViewController?
        {
            if theVC.splitViewItems.count > 1
            {
                return true
            }
        }
        
        return false
    }

    // ----------------------------
    // 改行キーで呼び出されていない。編集モードに入ってしまう
    @objc override func insertNewline(_ sender: Any?)
    {
        self.addItem(sender)
    }

    // ----------------------------
   @objc override func deleteForward(_ sender: Any?)
    {
        self.removeItem(sender)
    }

    // ----------------------------
    @objc override func deleteBackward(_ sender: Any?)
    {
        self.removeItem(sender)
    }
    
    // ----------------------------
    @objc override func moveUp(_ sender: Any?)
    {
        let theIndexSet: IndexSet = IndexSet([(self.arrayController!.selectionIndexes as NSIndexSet).firstIndex - 1])

        self.tableView?.selectRowIndexes(theIndexSet,
                                         byExtendingSelection: false)
    }
    
    // ----------------------------
    @objc override func moveDown(_ sender: Any?)
    {
        let theIndexSet: IndexSet = IndexSet([(self.arrayController!.selectionIndexes as NSIndexSet).firstIndex + 1])
        
        self.tableView?.selectRowIndexes(theIndexSet,
                                         byExtendingSelection: false)
    }

    // MARK: - event handling methods

    @objc override func keyDown(with event: NSEvent)
    {
        self.interpretKeyEvents([event])
    }

    // MARK: - drawing methods
    
    // MARK: - delegate/datasource methods
    
    // NSTableView Drag & Drop support
    
    @objc func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting?
    {
        let theArrangedObjects = self.arrayController?.arrangedObjects as! [ToDoItem]
        let theToDoItem = theArrangedObjects[row] 

        let thePboardItem: NSPasteboardItem  = NSPasteboardItem.init()

        theToDoItem.writableTypes(for:NSPasteboard.general).forEach
        {
            thePboardItem.setPropertyList(theToDoItem.pasteboardPropertyList(forType: $0)!,
                                          forType: $0)
        }
        
        return thePboardItem
    }
    
    
    @objc func tableView(_ tableView: NSTableView,
                         draggingSession session: NSDraggingSession,
                         willBeginAt screenPoint: NSPoint,
                         forRowIndexes rowIndexes: IndexSet)
    {
        // 表示されているTableViewからアニメーションつきの削除を実行するために
        // Dragの開始時に保存していた"storeDraggingSessionIndexSet"を保存

        self.storeDraggingSessionIndexSet = rowIndexes
    }

    
    @objc func tableView(_ tableView: NSTableView,
                         draggingSession session: NSDraggingSession,
                         endedAt screenPoint: NSPoint,
                         operation: NSDragOperation)
    {
        if operation == .delete
        {
            // クリップボードからToDoItemの配列を再生
            let ToDoItems: [ToDoItem] = session.draggingPasteboard.pasteboardItems!.map
            {
                ToDoItem.toDoItem(fromURL: $0, context: self.managedObjectContext!)!
            }

            // ローカル変数にコピーしているのは、アニメーション中に再びDragが開始される可能性があるから
            let theSelectedIndexes = self.storeDraggingSessionIndexSet
            
            // NSAnimationContextで包み、runで現在編集中のViewをアニメーションさせ, completionでモデルを変更する。
            // モデルに従属している他のViewはアニメーションの対象でなくてもOK
            NSAnimationContext.runAnimationGroup(
                {
                    context in
                    
                    self.tableView!.removeRows(at: theSelectedIndexes!, withAnimation: [.effectFade, .slideUp ])
                },
                completionHandler:
                {
                    self.arrayController!.remove(contentsOf: ToDoItems)
                    self.renumberingSortOrder()
                }
            )

            // 選択範囲をどうするか後で検討する
        }
    }
    
    @objc func tableView(_ tableView: NSTableView,
                         validateDrop info: NSDraggingInfo,
                         proposedRow row: Int,
                         proposedDropOperation dropOperation: NSTableView.DropOperation) -> NSDragOperation
    {
        let theSourceView     = info.draggingSource as? NSView
        let theSourceDicument = theSourceView?.value(forKeyPath: "window.windowController.document") as? NSDocument
        
        
        switch dropOperation
        {
            case .above:
                // 同じデータモデルの中でのDrag&Dropは移動である
                if  theSourceDicument == self.document
                    &&
                    !(NSApp.currentEvent!.modifierFlags.contains(.option))
                {
                    // Drag&Dropで移動しても、データの並びが変更しないパターンになる場合は、Dropは許可しない
                    // Drag中のsortOrderが連続している場合は、Dropできない箇所が生じる。
                    
                    // dragしているデータのsortOrderを確認すると連続かどうかがわかる
                    let theSortOrders: [Int32] = info.draggingPasteboard.pasteboardItems!.map
                    {
                        let theItem = ToDoItem.toDoItem(fromURL: $0, context: self.managedObjectContext!)!
                        return theItem.sortOrder
                    }

                    if theSortOrders.max()! - theSortOrders.min()! == theSortOrders.count - 1
                    {
                        // ドラッグ元が連続している場合には、Drag元と同じ箇所へDropしても変更が発生しないので、ドラッグ元の上か下にしかDropできない。
                        if row < theSortOrders.min()! || row > theSortOrders.max()! + 1
                        {
                            return .move
                        }
                        else
                        {
                            return []
                        }
                    }
                    else
                    {
                        // ドラッグ元が連続していない場合は、集約する動作があるため、どこにでもDropできる。
                        return .move
                    }
                }
                else
                {
                    return .copy
                }
            
            case .on:
                return []
        }
    }
    
    
    @objc func tableView(_ tableView: NSTableView,
                           acceptDrop info: NSDraggingInfo,
                           row: Int,
                           dropOperation: NSTableView.DropOperation) -> Bool
    {
        let theDragOperation = self.tableView(tableView,
                                              validateDrop: info,
                                              proposedRow: row,
                                              proposedDropOperation: dropOperation)
        
        switch  theDragOperation
        {
            case .move:
                let theArrangedObjects = self.arrayController?.arrangedObjects as! [ToDoItem]

                // 移動の影響範囲は、Drag元の範囲と、Drop先のrowの値から算出する
                let theDragItemSortOrders: [Int32] = info.draggingPasteboard.pasteboardItems!.map
                {
                    let theItem = ToDoItem.toDoItem(fromURL: $0, context: self.managedObjectContext!)!
                    return theItem.sortOrder
                }
                
                // 影響範囲の算出
                let theMin = min(Int(theDragItemSortOrders.min()!), row)
                // min関数があるのは挿入点が配列の末尾の場合に、添え字数が配列の上限より1超えてしまうのをガードするため
                let theMax = min(max(Int(theDragItemSortOrders.max()!), row), theArrangedObjects.count - 1)

                // Drag中のItemの配列を算出
                let theDragItems: [ToDoItem] = info.draggingPasteboard.pasteboardItems!.map
                {
                    return ToDoItem.toDoItem(fromURL: $0, context: self.managedObjectContext!)!
                }

                // 3つの部分に分ける。
                // (a) 挿入箇所より前の配列
                // (b) ドラック中の配列　　　-> theDragItems
                // (c) 挿入箇所より後の配列

                // 影響がある範囲だけを切り抜く
                let theLocalArrangedObject = theArrangedObjects[theMin...theMax]

                // ローカルでドラッグ中でない配列
                let theLocalUnDraggedObjects = theLocalArrangedObject.filter({ !theDragItems.contains($0) })
                
                // rowの場所で2つに分けて、bとcを作り出す
                let the_A_Part = theLocalUnDraggedObjects.filter({ $0.sortOrder < row })
                let the_B_Part = theDragItems
                let the_C_Part = theLocalUnDraggedObjects.filter({ $0.sortOrder >= row })
                
                // A、B、C、のsortOrderを再設定。
                var counter = Int32(theMin)
                
                the_A_Part.forEach({ $0.sortOrder = counter;  counter = counter + 1})
                the_B_Part.forEach({ $0.sortOrder = counter;  counter = counter + 1})
                the_C_Part.forEach({ $0.sortOrder = counter;  counter = counter + 1})

                // 遅延実行して追加した行を選択する。
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.1)
                {
                    self.arrayController?.setSelectedObjects(theDragItems)
                }

                return true
            
            case .copy:
                // sortOrderを後でずらすために現状の配列のrow以降の配列を摘出する。
                let theAfterArray = (self.arrayController!.arrangedObjects as! [ToDoItem]).dropFirst(row)
                
                // ペーストボードから読み込んだデータから配列を生成する
                let todoItemsFromPB: [ToDoItem] = info.draggingPasteboard.pasteboardItems!.map
                {
                    ToDoItem.toDoItem(fromPlist: $0, context: self.managedObjectContext!)!
                }
                
                // ペーストボードからのデータの挿入場所を開けるためにrow以降の配列のsortOrderを後ろにズラす。
                theAfterArray.forEach
                {
                    $0.sortOrder += Int32(todoItemsFromPB.count)
                }
            
                // ペーストボードから読み込んだ配列のsortOrderを設定する
                todoItemsFromPB.enumerated().forEach
                {
                    $0.1.sortOrder = Int32( $0.0 + row )
                }
                
                // 遅延実行して追加した行を選択する。
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.1)
                {
                    self.arrayController?.setSelectedObjects(todoItemsFromPB)
                }

                return true
            
            default:
                return false
        }
    }

    // MARK: - accessor methods (in pairs)

    @IBOutlet var tableView:   TDLTableView?

    @objc var arrayController: NSArrayController?
    {
        return self.value(forKeyPath: "windowController.arrayController") as? NSArrayController
    }
    
    @objc var managedObjectContext: NSManagedObjectContext?
    {
        return self.value(forKeyPath: "document.managedObjectContext") as? NSManagedObjectContext
    }
    
    @objc var storeDraggingSessionIndexSet: IndexSet?

    
    // MARK: - Utility methods
    
    func renumberingSortOrder()
    {
        let theArrangedObjects: NSArray = self.arrayController?.arrangedObjects as! NSArray
        let theArray:           NSArray = (theArrangedObjects).copy() as! NSArray
        
        theArrangedObjects.forEach
        {
            if let theItem = $0 as? ToDoItem
            {
                theItem.sortOrder = Int32(theArray.index(of: theItem))
            }
        }
    }
}
