;(function(global) {
  "use strict";

var Rpd = global.Rpd;
if (typeof Rpd === "undefined" && typeof require !== "undefined") {
    Rpd = require('rpd');
}

function stopPropagation(event) {
    event.stopPropagation();
    return event;
}

var ƒ = Rpd.unit;

var δ = Rpd.Render.data;

function svgNode(name) { return document.createElementNS(d3.namespaces.svg, name); }
function htmlNode(name) { return document.createElementNS(d3.namespaces.xhtml, name); }

/* ========================= util/number ========================= */

Rpd.channelrenderer('util/number', 'svg', {
    /* show: function(target, value) { }, */
    edit: function(target, inlet, valueIn) {
        var foElm = svgNode('foreignObject');
        foElm.setAttributeNS(null, 'width', 20);
        foElm.setAttributeNS(null, 'height', 30);
        var valInput = htmlNode('input');
        valInput.type = 'number';
        //valInput.style.position = 'absolute';
        valueIn.onValue(function(val) {
            valInput.value = val;
        });
        foElm.appendChild(valInput);
        target.appendChild(foElm);
        return Kefir.fromEvents(valInput, 'change')
                    .map(function() {
                        return valInput.value;
                    });
    }
});

/* ========================= util/wholenumber ========================= */

Rpd.channelrenderer('util/wholenumber', 'svg', Rpd.allChannelRenderers['util/number']['svg']);

/* ========================= util/random ========================= */

Rpd.noderenderer('util/random', 'svg', function() {
    return {
        size: { width: 40 }
    }
});

/* ========================= util/comment ========================= */

Rpd.noderenderer('util/comment', 'svg', function() {
    var textElm;
    return {
        size: { width: 100, height: 150 },
        first: function(bodyElm) {
            textElm = d3.select(bodyElm).append('text');
        },
        always: function(bodyElm, inlets, outlets) {
            if (inlets.width) textElm.attr('width', inlets.width);
            textElm.text(inlets.text || '<empty>');
        }
    }
});

/* ========================= util/log ========================= */

Rpd.noderenderer('util/log', 'svg', function() {
    var textElm;
    var capacity = 5;
    var savedValues = [];
    return {
        size: { width: 140, height: 30 },
        first: function(bodyElm) {
            textElm = d3.select(bodyElm).append('text');
        },
        always: function(bodyElm, inlets, outlets) {
            if (inlets.what) {
                if (savedValues.length > capacity) savedValues.shift();
                savedValues.push(inlets.what);
            }
            textElm.text((savedValues.length > 0) ? ('...' + savedValues.join(', ') + '.') : '...');
        }
    }
});

/* ========================= util/letter ========================= */

Rpd.noderenderer('util/letter', 'svg', function() {
    var textElm;
    return {
        first: function(bodyElm) {
            textElm = d3.select(bodyElm).append('text');
        },
        always: function(bodyElm, inlets, outlets) {
            textElm.text(outlets.letter);
        }
    }
});

/* ========================= util/bang ========================= */

Rpd.noderenderer('util/bang', 'svg', {
    size: { width: 30, height: 25 },
    first: function(bodyElm) {
        var circle = d3.select(svgNode('circle'))
                       .attr('r', 9).attr('fill', 'black')
                       .style('cursor', 'pointer')
                       .style('pointer-events', 'all');
        d3.select(bodyElm).append(ƒ(circle.node()));
        var circleClicks = Kefir.fromEvents(circle.node(), 'click');
        circleClicks.onValue(function() {
            circle.classed('rpd-util-bang-fresh', true);
        });
        circleClicks.delay(500).onValue(function() {
            circle.classed('rpd-util-bang-fresh', false);
        });
        return { 'trigger':
            { valueOut: circleClicks.map(function() { return {}; }) }
        };
    }
});

/* ========================= util/metro ========================= */

Rpd.noderenderer('util/metro', 'svg', function() {
    var circle;
    return {
        size: { width: 30, height: 25 },
        first: function(bodyElm) {
            circle = d3.select(svgNode('circle'))
                       .attr('r', 9).attr('fill', 'black')
                       .style('cursor', 'pointer')
                       .style('pointer-events', 'all');
            d3.select(bodyElm).append(ƒ(circle.node()));
        },
        always: function(bodyElm, inlets, outlets) {
            if (outlets.bang) {
                outlets.bang.onValue(function() {
                    circle.classed('rpd-util-metro-fresh', true);
                }).delay(500).onValue(function() {
                    circle.classed('rpd-util-metro-fresh', false);
                });
            }
        }
    }
});

/* ========================= util/palette ========================= */

/* Rpd.noderenderer('util/palette', 'svg', function() {
    var cellSide = 12;
    return {
        size: { width: 365, height: 60 },
        first: function(bodyElm) {
            var paletteChange = Kefir.emitter();
            var lastSelected, paletteGroups = [];
            d3.select(bodyElm)
              .append('g').attr('transform', 'translate(5, 0)')
              .call(function(target) {
                PALETTES.forEach(function(palette, i) {
                    target.append('g')
                          .attr('class', 'rpd-util-palette-variant')
                          .attr('transform', 'translate(' + (i * 14) + ', ' +
                                                            (-1 * (palette.length / 2 * cellSide)) + ')')
                          .call((function(palette) { return function(paletteGroup) {
                              palette.forEach(function(color, i) {
                                  paletteGroup.append('rect').attr('rx', 4)
                                              .attr('x', 0).attr('y', i * cellSide)
                                              .attr('width', cellSide).attr('height', cellSide)
                                              .attr('fill', color);
                              });
                              Kefir.fromEvents(paletteGroup.node(), 'click').onValue(function() {
                                  if (lastSelected) lastSelected.attr('class', 'rpd-util-palette-variant')
                                  paletteGroup.attr('class', 'rpd-util-palette-variant rpd-util-palette-active-variant');
                                  lastSelected = paletteGroup;
                                  paletteChange.emit(palette);
                              });
                              paletteGroups.push(paletteGroup);
                          } })(palette));
                });
            });
            lastSelected = paletteGroups[0];
            paletteGroups[0].attr('class', 'rpd-util-palette-variant rpd-util-palette-active-variant');
            return { 'selection': { valueOut: paletteChange } };
        }
    };
}); */

/* ========================= util/sum-of-three ========================= */

Rpd.noderenderer('util/sum-of-three', 'svg', function() {
    var textElement;
    return {
        //contentRule: 'replace',
        size: { width: 120, height: null },
        first: function(bodyElm) {
            textElement = svgNode('text');
            bodyElm.appendChild(textElement);
        },
        always: function(bodyElm, inlets, outlets) {
            textElement.innerHTML = '∑ (' + (inlets.a || '?') + ', '
                                          + (inlets.b || '?') + ', '
                                          + (inlets.c || '?') + ') = ' + (outlets.sum || '?');
        }
    }
});

/* ========================= util/knob & util/knobs ========================= */

var adaptToState = RpdUtils.adaptToState;

var defaultKnobConf = {
    speed: 1.5,
    radius: 13,
    width: 40, // radius * 2 + margin
    height: 40,
    //showIntTicks: false,
    //stickToInts: false,
    showGhost: true,
    adaptAngle: null,
    adaptValue: null
};

function createKnob(state, conf) {
    var lastValue = 0;
    //var state = { min: 0, max: 100 };

    var adaptAngle = conf.adaptAngle || function(s, v) { return v * 360; };

    return {
        init: function(parent, valueIn) {
            var hand, handGhost, face, text;
            var submit = Kefir.emitter();
            d3.select(parent)
              .call(function(bodyGroup) {
                  face = bodyGroup.append('circle').attr('r', conf.radius)
                                  .style('fill', 'rgba(200, 200, 200, .2)')
                                  .style('stroke-width', 2)
                                  .style('stroke', '#000');
                  handGhost = bodyGroup.append('line')
                                  .style('visibility', 'hidden')
                                  .attr('x1', 0).attr('y1', 0)
                                  .attr('x2', 0).attr('y2', conf.radius - 1)
                                  .style('stroke-width', 2)
                                  .style('stroke', 'rgba(255,255,255,0.1)');
                  hand = bodyGroup.append('line')
                                  .attr('x1', 0).attr('y1', 0)
                                  .attr('x2', 0).attr('y2', conf.radius)
                                  .style('stroke-width', 2)
                                  .style('stroke', '#000');
                  text = bodyGroup.append('text')
                                  .style('text-anchor', 'middle')
                                  .style('fill', '#fff')
                                  .text(0);
              });
            Kefir.fromEvents(parent, 'mousedown')
                 .map(stopPropagation)
                 .flatMap(function() {
                     if (conf.showGhost) handGhost.style('visibility', 'visible');
                     var values =
                        Kefir.fromEvents(document.body, 'mousemove')
                             //.throttle(16)
                             .takeUntilBy(Kefir.fromEvents(document.body, 'mouseup'))
                             .map(stopPropagation)
                             .map(function(event) {
                                 var faceRect = face.node().getBoundingClientRect();
                                 return { x: event.clientX - (faceRect.left + conf.radius),
                                          y: event.clientY - (faceRect.top + conf.radius) };
                             })
                             .map(function(coords) {
                                 var value = ((coords.y * conf.speed * -1) + 180) / 360;
                                 if (value < 0) {
                                     value = 0;
                                 } else if (value > 1) {
                                     value = 1;
                                 }
                                 return value;
                            });
                     values.last().onValue(function(val) {
                         lastValue = val;
                         handGhost.attr('transform', 'rotate(' + adaptAngle(state, lastValue) + ')')
                                  .style('visibility', 'hidden');
                         submit.emit(lastValue);
                     });
                     return values;
                 })
                 .merge(valueIn || Kefir.never())
                 .onValue(function(value) {
                     var valueText = adaptToState(state, value);
                     text.text(conf.adaptValue ? conf.adaptValue(valueText) : valueText);
                     hand.attr('transform', 'rotate(' + adaptAngle(state, value) + ')');
                 });
            return submit.merge(valueIn ? valueIn : Kefir.never());
        }
    }
}

function initKnobInGroup(knob, nodeRoot, id, count, width) {
    var submit;
    d3.select(nodeRoot).append('g')
      .attr('transform', 'translate(' + ((id * width) + (width / 2) - (count * width / 2)) + ',0)')
      .call(function(knobRoot) {
          knob.root = knobRoot;
          submit = knob.init(knobRoot.node());
      });
    return submit;
}

Rpd.noderenderer('util/knob', 'svg', function() {
    var state = { min: 0, max: 100 };
    var knob = createKnob(state, defaultKnobConf);

    return {
        size: { width: defaultKnobConf.width,
                height: defaultKnobConf.height },
        first: function(bodyElm, configurationIn) {
            var submit = knob.init(bodyElm,
                                   configurationIn.filter(function(c) { return c.hasOwnProperty('knob'); })
                                                  .map(function(c) { return c.knob; }));
            return {
                'knob': { valueOut: submit }
            };
        },
        always: function(bodyElm, inlets, outlets) {
            state.min = inlets.min || 0;
            state.max = inlets.max || 0;
        }
    };
});

Rpd.noderenderer('util/dial', 'svg', function() {
    // TODO: make it "sticky", so the hand would stick to int positions
    var state = { min: 0, max: 100 };
    var myKnobConf = {
        speed: defaultKnobConf.speed,
        radius: defaultKnobConf.radius,
        width: defaultKnobConf.width,
        height: defaultKnobConf.height,
        adaptValue: function(value) { return Math.floor(value); }
    };
    var knob = createKnob(state, myKnobConf);
    return {
        size: { width: myKnobConf.width,
                height: myKnobConf.height },
        first: function(bodyElm, configurationIn) {
            var submit = knob.init(bodyElm,
                                   configurationIn.filter(function(c) { return c.hasOwnProperty('dial'); })
                                                  .map(function(c) { return c.dial; }));
            return {
                'dial': { valueOut: submit }
            };
        },
        always: function(bodyElm, inlets, outlets) {
            state.min = inlets.min || 0;
            state.max = inlets.max || 0;
        }
    };
});

var DEFAULT_KNOB_COUNT = 4;

Rpd.noderenderer('util/knobs', 'svg', function() {
    var count = DEFAULT_KNOB_COUNT;
    var state = { min: 0, max: 100 };
    var knobs = [];
    for (var i = 0; i < count; i++) {
        knobs.push(createKnob(state, defaultKnobConf));
    }
    var nodeRoot;

    return {
        size: { width: count * defaultKnobConf.width, height: defaultKnobConf.height },
        //pivot: { x: 0, y: 0.5 },
        first: function(bodyElm) {
            var valueOut = Kefir.pool();
            nodeRoot = bodyElm;
            valueOut = Kefir.combine(
                knobs.map(function(knob, i) {
                    return initKnobInGroup(knob, nodeRoot, i, count, defaultKnobConf.width)
                           .merge(Kefir.constant(0));
                           // knob.init() returns stream of updates,
                           // so Kefir.combine will send every change
                })
            );
            return {
                'submit': { valueOut: valueOut }
            };
        },
        always: function(bodyElm, inlets, outlets) {
            state.min = inlets.min || 0;
            state.max = inlets.max || 0;
        }
    };
});

/* ========================= util/color ========================= */

var toHexColor = RpdUtils.toHexColor;

Rpd.noderenderer('util/color', 'svg', function() {
    var colorElm;
    return {
        size: { width: 50, height: 50 },
        first: function(bodyElm) {
            colorElm = svgNode('rect');
            colorElm.setAttributeNS(null, 'width', '30');
            colorElm.setAttributeNS(null, 'height', '30');
            colorElm.setAttributeNS(null, 'rx', '5');
            colorElm.setAttributeNS(null, 'ry', '5');
            colorElm.setAttributeNS(null, 'transform', 'translate(-15,-15)');
            colorElm.classList.add('rpd-util-color-display');
            bodyElm.appendChild(colorElm);
        },
        always: function(bodyElm, inlets, outlets) {
            colorElm.setAttributeNS(null, 'fill', toHexColor(outlets.color));
        }
    }
});

/* ========================= util/mouse-pos[-by-bang] ========================= */

// define separately to use for both `util/mouse-pos` and `util/mouse-pos-by-bang`
function mousePosNodeRenderer() {
    var radius = 20;
    var dirLine, dirCircle, posText, center;
    var xPosStream = Kefir.fromEvents(document.body, 'mousemove').throttle(16).map(function(evt) { return evt.x; });
    var yPosStream = Kefir.fromEvents(document.body, 'mousemove').throttle(16).map(function(evt) { return evt.y; });
    return {
        size: { width: radius * 2 + 10, height: radius * 2 + 10 },
        first: function(bodyElm) {
            var dirGroup = svgNode('g');
            dirCircle = svgNode('circle');
            dirLine = svgNode('line');
            posText = svgNode('text');

            dirLine.setAttributeNS(null, 'x1', 0);
            dirLine.setAttributeNS(null, 'y1', 0);
            dirLine.setAttributeNS(null, 'x2', 0);
            dirLine.setAttributeNS(null, 'y2', -radius);
            //dirLine.setAttributeNS(null, 'strokeWidth', 1);

            dirCircle.setAttributeNS(null, 'r', radius);

            dirGroup.appendChild(dirCircle);
            dirGroup.appendChild(dirLine);
            bodyElm.appendChild(dirGroup);
            bodyElm.appendChild(posText);

            return {
                x: { 'default': 0, valueOut: xPosStream },
                y: { 'default': 0, valueOut: yPosStream }
            }
        },
        always: function(bodyElm, inlets, outlets) {
            if (Number.isNaN(inlets.x) || Number.isNaN(inlets.y) || !inlets.y) return;
            var center = dirCircle.getBoundingClientRect();
            var angle = Math.atan2(inlets.y - (center.top + radius),
                                   inlets.x - (center.left + radius));
            dirLine.setAttributeNS(null, 'x2', Math.cos(angle) * radius);
            dirLine.setAttributeNS(null, 'y2', Math.sin(angle) * radius);
            posText.innerHTML = posText.innerText = '<' + inlets.x + ':' + inlets.y + '>';
        }
    }
}

Rpd.noderenderer('util/mouse-pos', 'svg', mousePosNodeRenderer);
Rpd.noderenderer('util/mouse-pos-by-bang', 'svg', mousePosNodeRenderer);

/* ========================= util/nodelist ========================= */

var NodeList = RpdUtils.NodeList;
var getNodeTypesByToolkit = RpdUtils.getNodeTypesByToolkit;

var nodeListSize = { width: 180, height: 300 };

var lineHeight = 22;  // find font-size?
var iconWidth = 11;
var inputWidth = nodeListSize.width - 40;
var inputHeight = 45;

Rpd.noderenderer('util/nodelist', 'svg', {
    size: nodeListSize,
    first: function(bodyElm) {

        var patch = this.patch;

        var nodeTypes = Rpd.allNodeTypes,
            nodeDescriptions = Rpd.allNodeDescriptions,
            toolkitIcons = Rpd.allToolkitIcons,
            nodeTypeIcons = Rpd.allNodeTypeIcons;

        var nodeTypesByToolkit = getNodeTypesByToolkit(nodeTypes);

        var bodyGroup = d3.select(bodyElm)
                           .append('g')
                           .attr('transform', 'translate(' + (-1 * nodeListSize.width / 2) + ','
                                                           + (-1 * nodeListSize.height / 2) + ')');
        var searchGroup = bodyGroup.append('g').classed('rpd-nodelist-search', true)
                                               .attr('transform', 'translate(12,12)');

        var nodeListSvg;

        var tookitElements = {},
            listElementsIdxByType = {};

        var nodeList = new NodeList({
            getPatch: function() { return patch; },
            buildList: function() {
                var listElements = [];

                var bodyRect = bodyGroup.node().getBoundingClientRect();

                var foreignDiv = bodyGroup.append(ƒ(svgNode('foreignObject')))
                                          .append(ƒ(htmlNode('div')))
                                          .style('width', (nodeListSize.width - 20) + 'px')
                                          .style('height', (nodeListSize.height - inputHeight) + 'px')
                                          .style('overflow-y', 'scroll')
                                         .style('position', 'fixed').style('left', 10 + 'px')
                                                                  .style('top', inputHeight + 'px');

                nodeListSvg = foreignDiv.append(ƒ(svgNode('svg')))
                                        .classed('rpd-nodelist-list', true)
                                        .attr('width', (nodeListSize.width - 12) + 'px');
                var lastY = 0;

                nodeListSvg.append('g')
                  .call(function(g) {
                      Object.keys(nodeTypesByToolkit).forEach(function(toolkit) {

                          var toolkitGroup = g.append('g').classed('rpd-nodelist-toolkit', true)
                                              .attr('transform', 'translate(0,' + lastY + ')')
                           .call(function(g) {
                                if (toolkitIcons[toolkit]) g.append('text').attr('class', 'rpd-nodelist-toolkit-icon').text(toolkitIcons[toolkit]);
                                g.append('text').attr('class', 'rpd-nodelist-toolkit-name').text(toolkit)
                           });

                          tookitElements[toolkit] = toolkitGroup;

                          lastY += lineHeight;

                          g.append('g').classed('rpd-nodelist-nodetypes', true)
                           .call(function(g) {
                                nodeTypesByToolkit[toolkit].types.forEach(function(nodeTypeDef) {
                                    var nodeType = nodeTypeDef.fullName;
                                    g.append('g').classed('rpd-nodelist-nodetype', true)
                                      .attr('transform', 'translate(0,' + lastY + ')')
                                     .call(function(g) {

                                          var hasDescription = nodeDescriptions[nodeType] ? true : false;

                                          var elmData = { def: nodeTypeDef,
                                                          element: g,
                                                          hasDescription: hasDescription,
                                                          initialY: lastY };

                                          δ(g, elmData);

                                          g.append('rect').attr('class', 'rpd-nodelist-item-bg')
                                                          .attr('x', 0).attr('y', -5).attr('rx', 5).attr('ry', 5)
                                                          .attr('width', nodeListSize.width - 20)
                                                          .attr('height', (hasDescription ? (lineHeight * 1.5) : lineHeight) - 5);
                                          g.append('text').attr('class', 'rpd-nodelist-icon').text(nodeTypeIcons[nodeType] || ' ')
                                                          .attr('x', (iconWidth / 2)).attr('y', 5);
                                          g.append('text').attr('class', 'rpd-nodelist-fulltypename')
                                                          .attr('transform', 'translate(' + (iconWidth + 4) + ',0)')
                                                          .text(nodeTypeDef.toolkit + '/' + nodeTypeDef.name)
                                          if (hasDescription) {
                                              lastY += (lineHeight * 0.5);
                                              g.select('rect').attr('title', nodeDescriptions[nodeType]);
                                              g.append('text').attr('class', 'rpd-nodelist-description')
                                                              .attr('transform', 'translate(3,' + (lineHeight * 0.6) + ')')
                                                              .text(nodeDescriptions[nodeType]);
                                          }

                                          listElements.push(elmData);

                                          listElementsIdxByType[nodeType] = listElements.length - 1;

                                          lastY += lineHeight;

                                      });
                                });
                           });

                      });
                  });

                nodeListSvg.attr('height', lastY + 'px');

                return listElements;
            },
            recalculateSize: function(listElements) {
                var lastY = 0;

                Object.keys(nodeTypesByToolkit).forEach(function(toolkit) {
                    tookitElements[toolkit].attr('transform', 'translate(0,' + lastY + ')');

                    lastY += lineHeight;

                    var elmDataIdx, elmData;

                    nodeTypesByToolkit[toolkit].types.forEach(function(nodeTypeDef) {
                        elmDataIdx = listElementsIdxByType[nodeTypeDef.fullName];
                        elmData = listElements[elmDataIdx];

                        if (elmData.visible) {
                            elmData.element.attr('transform', 'translate(0,' + lastY + ')');
                            lastY += lineHeight;
                            if (elmData.hasDescription) lastY += (lineHeight * 0.5);
                        }

                    });

                });

                nodeListSvg.attr('height', lastY + 'px');
            },
            createSearchInput: function() {
                var foElm = svgNode('foreignObject');
                foElm.setAttributeNS(null, 'width', inputWidth);
                foElm.setAttributeNS(null, 'height', 20);
                var input = htmlNode('input');
                input.setAttribute('type', 'text');
                input.style.width = inputWidth + 'px';
                foElm.appendChild(input);
                searchGroup.append(ƒ(foElm));
                return d3.select(input);
            },
            createClearSearchButton: function() {
                searchGroup.append('rect').attr('transform', 'translate(' + (nodeListSize.width - 32) + ',7)')
                           .attr('width', 12).attr('height', 12).attr('rx', 5);
                return searchGroup.append('text').text('x')
                                  .attr('transform', 'translate(' + (nodeListSize.width - 26) + ',12)');
            },
            clearSearchInput: function(searchInput) { searchInput.node().value = ''; },
            markSelected: function(elmData) { elmData.element.classed('rpd-nodelist-selected', true); },
            markDeselected: function(elmData) { elmData.element.classed('rpd-nodelist-selected', false); },
            markAdding: function(elmData) { elmData.element.classed('rpd-nodelist-add-effect', true); },
            markAdded: function(elmData) { elmData.element.classed('rpd-nodelist-add-effect', false); },
            setVisible: function(elmData) { elmData.element.style('display', 'list-item'); },
            setInvisible: function(elmData) { elmData.element.style('display', 'none'); }
        });

        nodeList.addOnClick();
        nodeList.addSearch();
        nodeList.addCtrlSpaceAndArrows();

    }
});

})(this);
