// SPDX-License-Identifier: GPL-2.0-or-later
/**
 * @file
 * Node selection - implementation.
 */
/* Authors:
 *   Krzysztof Kosiński <tweenk.pl@gmail.com>
 *
 * Copyright (C) 2009 Authors
 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
 */

#include <boost/none.hpp>
#include "ui/tool/selectable-control-point.h"
#include <2geom/transforms.h>
#include "desktop.h"
#include "ui/tool/control-point-selection.h"
#include "ui/tool/event-utils.h"
#include "ui/tool/transform-handle-set.h"
#include "ui/tool/node.h"
#include "display/control/snap-indicator.h"
#include "ui/widget/canvas.h"



#include <gdk/gdkkeysyms.h>

namespace Inkscape {
namespace UI {

/**
 * @class ControlPointSelection
 * Group of selected control points.
 *
 * Some operations can be performed on all selected points regardless of their type, therefore
 * this class is also a Manipulator. It handles the transformations of points using
 * the keyboard.
 *
 * The exposed interface is similar to that of an STL set. Internally, a hash map is used.
 * @todo Correct iterators (that don't expose the connection list)
 */

/** @var ControlPointSelection::signal_update
 * Fires when the display needs to be updated to reflect changes.
 */
/** @var ControlPointSelection::signal_point_changed
 * Fires when a control point is added to or removed from the selection.
 * The first param contains a pointer to the control point that changed sel. state. 
 * The second says whether the point is currently selected.
 */
/** @var ControlPointSelection::signal_commit
 * Fires when a change that needs to be committed to XML happens.
 */

ControlPointSelection::ControlPointSelection(SPDesktop *d, Inkscape::CanvasItemGroup *th_group)
    : Manipulator(d)
    , _handles(new TransformHandleSet(d, th_group))
    , _dragging(false)
    , _handles_visible(true)
    , _one_node_handles(false)
{
    signal_update.connect( sigc::bind(
        sigc::mem_fun(*this, &ControlPointSelection::_updateTransformHandles),
        true));
    ControlPoint::signal_mouseover_change.connect(
        sigc::hide(
            sigc::mem_fun(*this, &ControlPointSelection::_mouseoverChanged)));
    _handles->signal_transform.connect(
        sigc::mem_fun(*this, &ControlPointSelection::transform));
    _handles->signal_commit.connect(
        sigc::mem_fun(*this, &ControlPointSelection::_commitHandlesTransform));
}

ControlPointSelection::~ControlPointSelection()
{
    clear();
    delete _handles;
}

/** Add a control point to the selection. */
std::pair<ControlPointSelection::iterator, bool> ControlPointSelection::insert(const value_type &x, bool notify, bool to_update)
{
    iterator found = _points.find(x);
    if (found != _points.end()) {
        return std::pair<iterator, bool>(found, false);
    }

    found = _points.insert(x).first;
    _points_list.push_back(x);

    x->updateState();

    if (to_update) {
        _update();
    }
    if (notify) {
        signal_selection_changed.emit(std::vector<key_type>(1, x), true);
    }

    return std::pair<iterator, bool>(found, true);
}

/** Remove a point from the selection. */
void ControlPointSelection::erase(iterator pos, bool to_update)
{
    SelectableControlPoint *erased = *pos;
    _points_list.remove(*pos);
    _points.erase(pos);
    erased->updateState();
    if (to_update) {
        _update();
    }
}
ControlPointSelection::size_type ControlPointSelection::erase(const key_type &k, bool notify)
{
    iterator pos = _points.find(k);
    if (pos == _points.end()) return 0;
    erase(pos);

    if (notify) {
        signal_selection_changed.emit(std::vector<key_type>(1, k), false);
    }
    return 1;
}
void ControlPointSelection::erase(iterator first, iterator last)
{
    std::vector<SelectableControlPoint *> out(first, last);
    while (first != last) {
        erase(first++, false);
    }
    _update();
    signal_selection_changed.emit(out, false);
}

/** Remove all points from the selection, making it empty. */
void ControlPointSelection::clear()
{
    if (empty()) {
        return;
    }

    std::vector<SelectableControlPoint *> out(begin(), end()); // begin() takes from _points
    _points.clear();
    _points_list.clear();
    for (auto erased : out) {
        erased->updateState();
    }

    _update();
    signal_selection_changed.emit(out, false);
}

/** Select all points that this selection can contain. */
void ControlPointSelection::selectAll()
{
    for (auto _all_point : _all_points) {
        insert(_all_point, false, false);
    }
    std::vector<SelectableControlPoint *> out(_all_points.begin(), _all_points.end());
    if (!out.empty()) {
        _update();
        signal_selection_changed.emit(out, true);
    }
}
/** Select all points inside the given rectangle (in desktop coordinates). */
void ControlPointSelection::selectArea(Geom::Path const &path, bool invert)
{
    std::vector<SelectableControlPoint *> out;
    for (auto _all_point : _all_points) {
        if (path.winding(*_all_point) % 2 != 0) {
            if (invert) {
                erase(_all_point);
            } else {
                insert(_all_point, false, false);
            }
            out.push_back(_all_point);
        }
    }
    if (!out.empty()) {
        _update();
        signal_selection_changed.emit(out, true);
    }
}
/** Unselect all selected points and select all unselected points. */
void ControlPointSelection::invertSelection()
{
    std::vector<SelectableControlPoint *> in, out;
    for (auto _all_point : _all_points) {
        if (_all_point->selected()) {
            in.push_back(_all_point);
            erase(_all_point); 
        }
        else {
            out.push_back(_all_point);
            insert(_all_point, false, false); 
        }
    }
    _update();
    if (!in.empty())
        signal_selection_changed.emit(in, false);
    if (!out.empty())
        signal_selection_changed.emit(out, true);
}
void ControlPointSelection::spatialGrow(SelectableControlPoint *origin, int dir)
{
    bool grow = (dir > 0);
    Geom::Point p = origin->position();
    double best_dist = grow ? HUGE_VAL : 0;
    SelectableControlPoint *match = nullptr;
    for (auto _all_point : _all_points) {
        bool selected = _all_point->selected();
        if (grow && !selected) {
            double dist = Geom::distance(_all_point->position(), p);
            if (dist < best_dist) {
                best_dist = dist;
                match = _all_point;
            }
        }
        if (!grow && selected) {
            double dist = Geom::distance(_all_point->position(), p);
            // use >= to also deselect the origin node when it's the last one selected
            if (dist >= best_dist) {
                best_dist = dist;
                match = _all_point;
            }
        }
    }
    if (match) {
        if (grow) insert(match);
        else erase(match);
        signal_selection_changed.emit(std::vector<value_type>(1, match), grow);
    }
}

/** Transform all selected control points by the given affine transformation. */
void ControlPointSelection::transform(Geom::Affine const &m)
{
    for (auto cur : _points) {
        cur->transform(m);
    }
    for (auto cur : _points) {
        cur->fixNeighbors();
    }

    _updateBounds();
    // TODO preserving the rotation radius needs some rethinking...
    if (_rot_radius) (*_rot_radius) *= m.descrim();
    if (_mouseover_rot_radius) (*_mouseover_rot_radius) *= m.descrim();
    signal_update.emit();
}

/** Align control points on the specified axis. */
void ControlPointSelection::align(Geom::Dim2 axis, AlignTargetNode target)
{
    if (empty()) return;
    Geom::Dim2 d = static_cast<Geom::Dim2>((axis + 1) % 2);

    Geom::OptInterval bound;
    for (auto _point : _points) {
        bound.unionWith(Geom::OptInterval(_point->position()[d]));
    }

    if (!bound) { return; }

    double new_coord;
    switch (target) {
        case AlignTargetNode::FIRST_NODE:
            new_coord=(_points_list.front())->position()[d];
            break;
        case AlignTargetNode::LAST_NODE:
            new_coord=(_points_list.back())->position()[d];
            break;
        case AlignTargetNode::MID_NODE:
            new_coord=bound->middle();
            break;
        case AlignTargetNode::MIN_NODE:
            new_coord=bound->min();
            break;
        case AlignTargetNode::MAX_NODE:
            new_coord=bound->max();
            break;
        default:
            return;
    }

    for (auto _point : _points) {
        Geom::Point pos = _point->position();
        pos[d] = new_coord;
        _point->move(pos);
    }
}

/** Equdistantly distribute control points by moving them in the specified dimension. */
void ControlPointSelection::distribute(Geom::Dim2 d)
{
    if (empty()) return;

    // this needs to be a multimap, otherwise it will fail when some points have the same coord
    typedef std::multimap<double, SelectableControlPoint*> SortMap;

    SortMap sm;
    Geom::OptInterval bound;
    // first we insert all points into a multimap keyed by the aligned coord to sort them
    // simultaneously we compute the extent of selection
    for (auto _point : _points) {
        Geom::Point pos = _point->position();
        sm.insert(std::make_pair(pos[d], _point));
        bound.unionWith(Geom::OptInterval(pos[d]));
    }

    if (!bound) { return; }

    // now we iterate over the multimap and set aligned positions.
    double step = size() == 1 ? 0 : bound->extent() / (size() - 1);
    double start = bound->min();
    unsigned num = 0;
    for (SortMap::iterator i = sm.begin(); i != sm.end(); ++i, ++num) {
        Geom::Point pos = i->second->position();
        pos[d] = start + num * step;
        i->second->move(pos);
    }
}

/** Get the bounds of the selection.
 * @return Smallest rectangle containing the positions of all selected points,
 *         or nothing if the selection is empty */
Geom::OptRect ControlPointSelection::pointwiseBounds()
{
    return _bounds;
}

Geom::OptRect ControlPointSelection::bounds()
{
    return size() == 1 ? (*_points.begin())->bounds() : _bounds;
}

void ControlPointSelection::showTransformHandles(bool v, bool one_node)
{
    _one_node_handles = one_node;
    _handles_visible = v;
    _updateTransformHandles(false);
}

void ControlPointSelection::hideTransformHandles()
{
    _handles->setVisible(false);
}
void ControlPointSelection::restoreTransformHandles()
{
    _updateTransformHandles(true);
}

void ControlPointSelection::toggleTransformHandlesMode()
{
    if (_handles->mode() == TransformHandleSet::MODE_SCALE) {
        _handles->setMode(TransformHandleSet::MODE_ROTATE_SKEW);
        if (size() == 1) {
            _handles->rotationCenter().setVisible(false);
        }
    } else {
        _handles->setMode(TransformHandleSet::MODE_SCALE);
    }
}

void ControlPointSelection::_pointGrabbed(SelectableControlPoint *point)
{
    hideTransformHandles();
    _dragging = true;
    _grabbed_point = point;
    _farthest_point = point;
    double maxdist = 0;
    Geom::Affine m;
    m.setIdentity();
    for (auto _point : _points) {
        _original_positions.insert(std::make_pair(_point, _point->position()));
        _last_trans.insert(std::make_pair(_point, m));
        double dist = Geom::distance(*_grabbed_point, *_point);
        if (dist > maxdist) {
            maxdist = dist;
            _farthest_point = _point;
        }
    }
}

void ControlPointSelection::_pointDragged(Geom::Point &new_pos, GdkEventMotion *event)
{
    Geom::Point abs_delta = new_pos - _original_positions[_grabbed_point];
    double fdist = Geom::distance(_original_positions[_grabbed_point], _original_positions[_farthest_point]);
    if (held_only_alt(*event) && fdist > 0) {
        // Sculpting
        for (auto cur : _points) {
            Geom::Affine trans;
            trans.setIdentity();
            double dist = Geom::distance(_original_positions[cur], _original_positions[_grabbed_point]);
            double deltafrac = 0.5 + 0.5 * cos(M_PI * dist/fdist);
            if (dist != 0.0) {
                // The sculpting transformation is not affine, but it can be
                // locally approximated by one. Here we compute the local
                // affine approximation of the sculpting transformation near
                // the currently transformed point. We then transform the point
                // by this approximation. This gives us sensible behavior for node handles.
                // NOTE: probably it would be better to transform the node handles,
                // but ControlPointSelection is supposed to work for any
                // SelectableControlPoints, not only Nodes. We could create a specialized
                // NodeSelection class that inherits from this one and move sculpting there.
                Geom::Point origdx(Geom::EPSILON, 0);
                Geom::Point origdy(0, Geom::EPSILON);
                Geom::Point origp = _original_positions[cur];
                Geom::Point origpx = _original_positions[cur] + origdx;
                Geom::Point origpy = _original_positions[cur] + origdy;
                double distdx = Geom::distance(origpx, _original_positions[_grabbed_point]);
                double distdy = Geom::distance(origpy, _original_positions[_grabbed_point]);
                double deltafracdx = 0.5 + 0.5 * cos(M_PI * distdx/fdist);
                double deltafracdy = 0.5 + 0.5 * cos(M_PI * distdy/fdist);
                Geom::Point newp = origp + abs_delta * deltafrac;
                Geom::Point newpx = origpx + abs_delta * deltafracdx;
                Geom::Point newpy = origpy + abs_delta * deltafracdy;
                Geom::Point newdx = (newpx - newp) / Geom::EPSILON;
                Geom::Point newdy = (newpy - newp) / Geom::EPSILON;

                Geom::Affine itrans(newdx[Geom::X], newdx[Geom::Y], newdy[Geom::X], newdy[Geom::Y], 0, 0);
                if (itrans.isSingular())
                    itrans.setIdentity();

                trans *= Geom::Translate(-cur->position());
                trans *= _last_trans[cur].inverse();
                trans *= itrans;
                trans *= Geom::Translate(_original_positions[cur] + abs_delta * deltafrac);
                _last_trans[cur] = itrans;
            } else {
                trans *= Geom::Translate(-cur->position() + _original_positions[cur] + abs_delta * deltafrac);
            }
            cur->transform(trans);
            //cur->move(_original_positions[cur] + abs_delta * deltafrac);
        }
    } else {
        Geom::Point delta = new_pos - _grabbed_point->position();
        for (auto cur : _points) {
            cur->move(_original_positions[cur] + abs_delta);
        }
        _handles->rotationCenter().move(_handles->rotationCenter().position() + delta);
    }
    for (auto cur : _points) {
        cur->fixNeighbors();
    }
    signal_update.emit();
}

void ControlPointSelection::_pointUngrabbed()
{
    _desktop->snapindicator->remove_snaptarget();
    _original_positions.clear();
    _last_trans.clear();
    _dragging = false;
    _grabbed_point = _farthest_point = nullptr;
    _updateBounds();
    restoreTransformHandles();
    signal_commit.emit(COMMIT_MOUSE_MOVE);
}

bool ControlPointSelection::_pointClicked(SelectableControlPoint *p, GdkEventButton *event)
{
    // clicking a selected node should toggle the transform handles between rotate and scale mode,
    // if they are visible
    if (held_no_modifiers(*event) && _handles_visible && p->selected()) {
        toggleTransformHandlesMode();
        return true;
    }
    return false;
}

void ControlPointSelection::_mouseoverChanged()
{
    _mouseover_rot_radius = std::nullopt;
}

void ControlPointSelection::_update()
{
    _updateBounds();
    _updateTransformHandles(false);
    if (_bounds) {
        _handles->rotationCenter().move(_bounds->midpoint());
    }
}

void ControlPointSelection::_updateBounds()
{
    _rot_radius = std::nullopt;
    _bounds = Geom::OptRect();
    for (auto cur : _points) {
        Geom::Point p = cur->position();
        if (!_bounds) {
            _bounds = Geom::Rect(p, p);
        } else {
            _bounds->expandTo(p);
        }
    }
}

void ControlPointSelection::_updateTransformHandles(bool preserve_center)
{
    if (_dragging) return;

    if (_handles_visible && size() > 1) {
        _handles->setBounds(*bounds(), preserve_center);
        _handles->setVisible(true);
    } else if (_one_node_handles && size() == 1) { // only one control point in selection
        SelectableControlPoint *p = *begin();
        _handles->setBounds(p->bounds());
        _handles->rotationCenter().move(p->position());
        _handles->rotationCenter().setVisible(false);
        _handles->setVisible(true);
    } else {
        _handles->setVisible(false);
    }
}

/** Moves the selected points along the supplied unit vector according to
 * the modifier state of the supplied event. */
bool ControlPointSelection::_keyboardMove(GdkEventKey const &event, Geom::Point const &dir)
{
    if (held_control(event)) return false;
    unsigned num = 1 + Tools::gobble_key_events(shortcut_key(event), 0);

    Geom::Point delta = dir * num; 
    if (held_shift(event)) delta *= 10;
    if (held_alt(event)) {
        delta /= _desktop->current_zoom();
    } else {
        Inkscape::Preferences *prefs = Inkscape::Preferences::get();
        double nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000, "px");
        delta *= nudge;
    }

    transform(Geom::Translate(delta));
    if (fabs(dir[Geom::X]) > 0) {
        signal_commit.emit(COMMIT_KEYBOARD_MOVE_X);
    } else {
        signal_commit.emit(COMMIT_KEYBOARD_MOVE_Y);
    }
    return true;
}

/**
 * Computes the distance to the farthest corner of the bounding box.
 * Used to determine what it means to "rotate by one pixel".
 */
double ControlPointSelection::_rotationRadius(Geom::Point const &rc)
{
    if (empty()) return 1.0; // some safe value
    Geom::Rect b = *bounds();
    double maxlen = 0;
    for (unsigned i = 0; i < 4; ++i) {
        double len = Geom::distance(b.corner(i), rc);
        if (len > maxlen) maxlen = len;
    }
    return maxlen;
}

/**
 * Rotates the selected points in the given direction according to the modifier state
 * from the supplied event.
 * @param event Key event to take modifier state from
 * @param dir   Direction of rotation (math convention: 1 = counterclockwise, -1 = clockwise)
 */
bool ControlPointSelection::_keyboardRotate(GdkEventKey const &event, int dir)
{
    if (empty()) return false;

    Geom::Point rc;

    // rotate around the mouseovered point, or the selection's rotation center
    // if nothing is mouseovered
    double radius;
    SelectableControlPoint *scp =
        dynamic_cast<SelectableControlPoint*>(ControlPoint::mouseovered_point);
    if (scp) {
        rc = scp->position();
        if (!_mouseover_rot_radius) {
            _mouseover_rot_radius = _rotationRadius(rc);
        }
        radius = *_mouseover_rot_radius;
    } else {
        rc = _handles->rotationCenter();
        if (!_rot_radius) {
            _rot_radius = _rotationRadius(rc);
        }
        radius = *_rot_radius;
    }

    double angle;
    if (held_alt(event)) {
        // Rotate by "one pixel". We interpret this as rotating by an angle that causes
        // the topmost point of a circle circumscribed about the selection's bounding box
        // to move on an arc 1 screen pixel long.
        angle = atan2(1.0 / _desktop->current_zoom(), radius) * dir;
    } else {
        Inkscape::Preferences *prefs = Inkscape::Preferences::get();
        int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
        angle = M_PI * dir / snaps;
    }

    // translate to origin, rotate, translate back to original position
    Geom::Affine m = Geom::Translate(-rc)
        * Geom::Rotate(angle) * Geom::Translate(rc);
    transform(m);
    signal_commit.emit(COMMIT_KEYBOARD_ROTATE);
    return true;
}


bool ControlPointSelection::_keyboardScale(GdkEventKey const &event, int dir)
{
    if (empty()) return false;

    double maxext = bounds()->maxExtent();
    if (Geom::are_near(maxext, 0)) return false;

    Geom::Point center;
    SelectableControlPoint *scp =
        dynamic_cast<SelectableControlPoint*>(ControlPoint::mouseovered_point);
    if (scp) {
        center = scp->position();
    } else {
        center = _handles->rotationCenter().position();
    }

    double length_change;
    if (held_alt(event)) {
        // Scale by "one pixel". It means shrink/grow 1px for the larger dimension
        // of the bounding box.
        length_change = 1.0 / _desktop->current_zoom() * dir;
    } else {
        Inkscape::Preferences *prefs = Inkscape::Preferences::get();
        length_change = prefs->getDoubleLimited("/options/defaultscale/value", 2, 1, 1000, "px");
        length_change *= dir;
    }
    double scale = (maxext + length_change) / maxext;
    
    Geom::Affine m = Geom::Translate(-center) * Geom::Scale(scale) * Geom::Translate(center);
    transform(m);
    signal_commit.emit(COMMIT_KEYBOARD_SCALE_UNIFORM);
    return true;
}

bool ControlPointSelection::_keyboardFlip(Geom::Dim2 d)
{
    if (empty()) return false;

    Geom::Scale scale_transform(1, 1);
    if (d == Geom::X) {
        scale_transform = Geom::Scale(-1, 1);
    } else {
        scale_transform = Geom::Scale(1, -1);
    }

    SelectableControlPoint *scp =
        dynamic_cast<SelectableControlPoint*>(ControlPoint::mouseovered_point);
    Geom::Point center = scp ? scp->position() : _handles->rotationCenter().position();

    Geom::Affine m = Geom::Translate(-center) * scale_transform * Geom::Translate(center);
    transform(m);
    signal_commit.emit(d == Geom::X ? COMMIT_FLIP_X : COMMIT_FLIP_Y);
    return true;
}

void ControlPointSelection::_commitHandlesTransform(CommitEvent ce)
{
    _updateBounds();
    _updateTransformHandles(true);
    signal_commit.emit(ce);
}

bool ControlPointSelection::event(Inkscape::UI::Tools::ToolBase * /*event_context*/, GdkEvent *event)
{
    // implement generic event handling that should apply for all control point selections here;
    // for example, keyboard moves and transformations. This way this functionality doesn't need
    // to be duplicated in many places
    // Later split out so that it can be reused in object selection

    switch (event->type) {
    case GDK_KEY_PRESS:
        // do not handle key events if the selection is empty
        if (empty()) break;

        switch(shortcut_key(event->key)) {
        // moves
        case GDK_KEY_Up:
        case GDK_KEY_KP_Up:
        case GDK_KEY_KP_8:
            return _keyboardMove(event->key, Geom::Point(0, -_desktop->yaxisdir()));
        case GDK_KEY_Down:
        case GDK_KEY_KP_Down:
        case GDK_KEY_KP_2:
            return _keyboardMove(event->key, Geom::Point(0, _desktop->yaxisdir()));
        case GDK_KEY_Right:
        case GDK_KEY_KP_Right:
        case GDK_KEY_KP_6:
            return _keyboardMove(event->key, Geom::Point(1, 0));
        case GDK_KEY_Left:
        case GDK_KEY_KP_Left:
        case GDK_KEY_KP_4:
            return _keyboardMove(event->key, Geom::Point(-1, 0));

        // rotates
        case GDK_KEY_bracketleft:
            return _keyboardRotate(event->key, -_desktop->yaxisdir());
        case GDK_KEY_bracketright:
            return _keyboardRotate(event->key, _desktop->yaxisdir());

        // scaling
        case GDK_KEY_less:
        case GDK_KEY_comma:
            return _keyboardScale(event->key, -1);
        case GDK_KEY_greater:
        case GDK_KEY_period:
            return _keyboardScale(event->key, 1);

        // TODO: skewing

        // flipping
        // NOTE: H is horizontal flip, while Shift+H switches transform handle mode!
        case GDK_KEY_h:
        case GDK_KEY_H:
            if (held_shift(event->key)) {
                toggleTransformHandlesMode();
                return true;
            }
            // any modifiers except shift should cause no action
            if (held_any_modifiers(event->key)) break;
            return _keyboardFlip(Geom::X);
        case GDK_KEY_v:
        case GDK_KEY_V:
            if (held_any_modifiers(event->key)) break;
            return _keyboardFlip(Geom::Y);
        default: break;
        }
        break;
    default: break;
    }
    return false;
}

void ControlPointSelection::getOriginalPoints(std::vector<Inkscape::SnapCandidatePoint> &pts)
{
    pts.clear();
    for (auto _point : _points) {
        pts.emplace_back(_original_positions[_point], SNAPSOURCE_NODE_HANDLE);
    }
}

void ControlPointSelection::getUnselectedPoints(std::vector<Inkscape::SnapCandidatePoint> &pts)
{
    pts.clear();
    ControlPointSelection::Set &nodes = this->allPoints();
    for (auto node : nodes) {
        if (!node->selected()) {
            Node *n = static_cast<Node*>(node);
            pts.push_back(n->snapCandidatePoint());
        }
    }
}

void ControlPointSelection::setOriginalPoints()
{
    _original_positions.clear();
    for (auto _point : _points) {
        _original_positions.insert(std::make_pair(_point, _point->position()));
    }
}

} // namespace UI
} // namespace Inkscape

/*
  Local Variables:
  mode:c++
  c-file-style:"stroustrup"
  c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
  indent-tabs-mode:nil
  fill-column:99
  End:
*/
// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
